О модальных окнах написано уже, наверное, тонны литературы, но на написание этого топика меня меня сподвигла вот эта статья на хабре. В ней осталось много недосказанного, в том числе горизонтальные скачки как страницы, так и модального окна.
Конечно, можно было бы просто отписаться в комментарих, мол, сделайте так-то и так-то, и все будет тип-топ. Но мой комментарий разросся до размеров новой статьи, с наглядными примерами и комментариями.
Кому стало интересно — добро пожаловать под хабракат!
Прямо скажу, что данный вид просмотра контента на странице называть «модальным окном» у меня язык не поворачивается. Все-таки это немного разные вещи. Поэтому я буду называть его «облачный контент». Думаю, это наиболее правильное определение для подобного вида отображения контента.
Итак, наши основные задачи:
1) умудриться зафиксировать страницу так, чтобы при горячем изменении ее высоты она не «прыгала» вправо-влево.
2) открыть облачное окно и отцентрировать его так, чтобы и при изменении и его высоты контент на странице оставался на месте.
3) добиться вертикальной прокрутки «облачного окна» при помощи клавиш на клавиатуре «Стрелки вверх и вниз».
4) ранее открытые окна кешировать в DOM, дабы не производить сложных действий и вычислений или не делать лишний запрос к серверу.
5) подключить горячие навигационные клавиши для максимально удобной навигации конечного пользователя.
Пункты 4 и 5 и будут плюшками. Собственно, начнем.
Сразу покажу готовый наглядный пример, чтобы при чтении данной статьи у вас было с чем сравнить и посмотреть, о чем конкретно идет речь.
ДЕМО ПРИМЕР и подключенный к странице JS файл
Для начала мы определим функцию elemID(); Так как конструкция document.getElementById встречается в коде довольно часто, проще укоротить эту запись при помощи подобной функции, что позволяет значительно уменьшить общий вес JS файла. Это особо важно при массивных файлах с большим объемом кода.
function elemID(e) {return document.getElementById(e);};
Заводим объект, с которым будем впоследствии работать:
var Content = {}
Первое, с чего мы начнем — это фиксация страницы. Для этого мы весь контент страницы обернем в блок с id=«layer»
<body id="body">
<div class="layer" id="layer">
<!-- здесь прочий контент сайта -->
</div>
</body>
Готово. Теперь для него пишем метод, центрирующий этот блок:
editSiteLayer:function (content) {
/** центрировать можно любой элемент, но по умолчанию - "layer" **/
if (!content) {var content = 'layer';}
/**
** ширина окна браузера обозревателя
** за вычетом 18px - стандартной ширины полосы прокрутки
**/
var inWidth = window.innerWidth-18,
/** ширина центрируемого контента **/
layerWidth = elemID(content).clientWidth;
/** если оба значения найдены и они не undefined **/
if (inWidth && layerWidth) {
/** задание блоку с контентом сайта CSS значения float:left **/
elemID(content).style.cssFloat = 'left';
/**
** чтобы контент сайта не прижимался к левому краю окна браузера - центрирование этого элемента
** за счет разницы половины ширины окна обозревателя и половины ширины контента
**/
elemID(content).style.marginLeft = (inWidth/2)-(layerWidth/2)+'px';
}
}
Написали и временно забыли о нем (в конце мы обязательно подключим его).
Вторая наша задача — это создать и открыть «облачное окно».
Открывать облачное окно мы будем так:
<body id="body">
<div class="cloud" id="cloud">
<div class="cloud-layer" id="cloud-layer">
<em class="cloud-contaner" id="cloudpage_уникальный_ID_блока">
</em>
</div>
</div>
</body>
Блок «cloud» выполняет роль некой защитной маски и отделяет контент сайта от облачного окна, при этом и делая его «облачным». Его CSS свойства:
.cloud{
display:block;
padding:0;
margin:0;
position:fixed;
top:0; left:0; right:0; bottom:0;
background: rgba(0, 0, 0, 0.795);
z-index:3;
overflow:auto;
}
Блок «cloud-layer» является «родителем» для всех DOM элементов с облачным контентом. Фиксируется он по центру страницы и ему задается свойство position:relative; (именно оно позволяет скроллить окно клавишами клавиатуры).
Его CSS свойства:
.cloud .cloud-layer{
position:relative;
float:left;
width:720px;
margin:0;
padding:0;
z-index:4;
}
Блок «cloud-contaner» — это и есть контейнер для нашего облачного контента. Почему именно в теге EM? Впоследствии мы будем пробегаться по DOM в поиске актуального блока и с целью скрыть все уже существующие. Поиск по тегу для нас подходит как нельзя кстати.
Чтобы понять реальную задачу — представим, что нам хочется просмотреть все личные фотографии пользователя на нашем сайте. Обычно делается так: при первом нажатии на фотографию делается http-запрос к серверу для получения массива всех фотографий, их уникальных ID, описанием и прочей информацией.
Для примера я не буду этого делать, а заранее руками прописал этот массив и приготовил его к работе:
var photoInfo = [];
photoInfo[0] = {'id':5478,'name':'Название фото №1','desc':'Описание фото №1','link':'1347690631.jpg'};
photoInfo[1] = {'id':4198,'name':'Название фото №2','desc':'Описание фото №2','link':'1347691505.jpg'};
photoInfo[2] = {'id':7596,'name':'Название фото №3','desc':'Описание фото №3','link':'1347691550.jpg'};
photoInfo[3] = {'id':98637,'name':'Название фото №4','desc':'Описание фото №4','link':'1347691521.jpg'};
Теперь при открытии окна мы опираемся на уникальный ID фотографии, перебираем массив, выбираем из него нужную информацию по текущему фото, а так же ID предыдущего и следующего фото для навигации. Параллельно пробегаемся по DOM и проверяем, существует ли в DOM актуальный контент. Если существует — открываем его, если нет — генерируем новый блок и вставляем его в облако. Но для начала проверяем, есть ли блок с облаком в DOM или еще нет. Если еще нет и это первое открытие облака — генерация облака, а в нем — генерация родителя облачного контента.
/**
* метод cloudNav() является вспомогательным для метода cloudShow()
* и выполняет функцию определения предыдущей и следующей фотографии
**/
_cloudNav:function (gid) {
if (photoInfo) {
var num, prev, next, info, len = photoInfo.length;
if (len > 0) {
for(var i=0; i<len; i++) {
if (photoInfo[i].id == gid) {
current = i;
info = photoInfo[i];
if (len > 1 && (len-1) > i) {next = photoInfo[i+1].id;}
if (len > 1 && i > 0) {prev = photoInfo[i-1].id;}
num = i+1;
}
}
}
return {"len":len,"num":num,"prev":prev,"current":gid,"next":next,"info":info};
}
},
/**
* метод cloudShow() генерирует облачное окно, определяет контент и показывает пользователю
* gid - уникальный идентификатор фотографии в системе
**/
cloudShow:function (gid) {
/** определение инедтификаторов предыдущей, текущей и следующей фотографии **/
var arrNav = Content._cloudNav(gid);
if (arrNav && arrNav.info) {
/** определение идентификатора для горячих навигационных клавиш **/
if (arrNav.prev !== undefined) {ContentPointPrev = arrNav.prev;} else {ContentPointPrev = null;}
if (arrNav.next !== undefined) {ContentPointNext = arrNav.next;} else {ContentPointNext = null;}
/** фиксация общей страницы, дабы убить ее прокручивание под облачным контентом и убрать скролл-бар, если он есть **/
if (elemID('body').style.overflowY != 'hidden') {
elemID('body').style.overflowX = 'hidden';
elemID('body').style.overflowY = 'hidden';
}
/** если блока для воздушного окна еще нет - создание этого блока **/
if (!elemID('cloud')) {
/** создание основного фиксированного полупрозрачного слоя "cloud" **/
var box = document.createElement('div');
box.className = 'cloud';
box.id = 'cloud';
elemID('body').appendChild(box);
/** помещение в слой "cloud" блока-родителя для блоков фотографий **/
var cloudbox = document.createElement('div');
cloudbox.className = 'cloud-layer';
cloudbox.id = 'cloud-layer';
elemID('cloud').appendChild(cloudbox);
/** помещение в слой "cloud" навигационного блока для кнопок Вперед/Назад/Закрыть **/
var navbox = document.createElement('div');
navbox.id = 'cloud-nav';
elemID('cloud').appendChild(navbox);
/** центрирование облачного контента по центру страницы **/
Content.editSiteLayer('cloud-layer');
/** В противном случае - перебор существующих в нем блоков с тегом EM и скрытие их **/
} else {
var ems = elemID('cloud').getElementsByTagName('EM')
if (ems.length > 0) {
for(var i=0; i<ems.length; i++) {
ems[i].style.display = 'none';
}
}
}
/** очищение навигационного блока **/
elemID('cloud-nav').innerHTML = '';
/** если необходимого блока с контентом еще нет - создание этого блока **/
if (!elemID('cloudpage_'+gid)) {
var contentbox = document.createElement('em');
contentbox.className = 'cloud-contaner';
contentbox.id = 'cloudpage_'+gid;
elemID('cloud-layer').appendChild(contentbox);
var html =
'<div class="cloud-title">'+
'<div class="cloud-name">'+
'Фотография '+arrNav.num+' из '+arrNav.len+
'</div>'+
'<div class="cloud-close" onclick="return Content.cloudClose();">Закрыть окно</div>'+
'</div>'+
'<div class="cloud-body">'+
'<p><img onclick="'+(arrNav.next !== undefined ? 'return Content.cloudShow('+arrNav.next+')' : 'return Content.cloudClose();')+'" src="images/'+arrNav.info.link+'" alt="" /></p>'+
'<div class="more-button" onclick="Content.Slide(this, {point:'next', hide:'Нажмите для удлинения страницы', show:'Нажмите для укорачивания страницы'})">'+
'Нажмите для удлинения страницы'+
'</div>'+
'<span style="display:none;">'+
'<p>'+arrNav.info.name+'</p>'+
'<p>'+arrNav.info.desc+'</p>'+
'<p>'+lorem[0]+'</p>'+
'<p>'+lorem[1]+'</p>'+
'<p>'+lorem[0]+'</p>'+
'<p>'+lorem[1]+'</p>'+
'<p>'+lorem[0]+'</p>'+
'<p>'+lorem[1]+'</p>'+
'</span>'+
'</div>';
elemID('cloudpage_'+gid).innerHTML = html;
/** в противном случае просто отображение этого блока **/
} else {
elemID('cloudpage_'+gid).style.display = 'block';
}
/** определение навигационных клавиш **/
var navi = '<div class="cloudnavclose" onclick="return Content.cloudClose();"></div>';
if (arrNav.prev !== undefined) {navi+= '<div onclick="return Content.cloudShow('+arrNav.prev+')" class="cloudnavprev"></div>';}
if (arrNav.next !== undefined) {navi+= '<div onclick="return Content.cloudShow('+arrNav.next+')" class="cloudnavnext"></div>';}
elemID('cloud-nav').innerHTML = navi;
/** инентификатор того, что окно октыто **/
ContentShow = true;
/** отображение пользователю на странице **/
elemID('cloud').style.display = 'block';
}
},
Выше я изпользовал некоторые значения массива «lorem» — в нем я заранее заготовил очень длинный текст для наглядного удлинения страницы, дабы показать, что окно зафиксировано.
При открытии окна заюзан метод Content.editSiteLayer('cloud-layer'); — уже ранее описанным методом мы центрируем облачное окно по центру страницы, не позаоляя ему скакать вправо-влево при появлении полосы прокрутки.
При закрытии окна используется метод Content.cloudClose(); Вот он:
/** метод cloudClose() скрывает облачное окно **/
cloudClose:function () {
/** скрытие общего облака со страницы **/
elemID('cloud').style.display = 'none';
/** фиксировать основной контент страницы больше не нужно **/
elemID('body').style.overflowX = 'auto';
elemID('body').style.overflowY = 'auto';
/** обнуление идентификатора открытого облачного окна **/
ContentShow = null;
}
И последняя наша задача — это подключение горячих клавиш. Для этого мы создадим метод Content.init(), который будет обрабатывать горячие клавиши и при загрузке страницы сразу центрировать ее в окне обозревателя (то, о чем я писал в самом начале статьи):
/**
* метод init() стартует обработку облачного контента и обрабатывает горячие клавиши
**/
init:function () {
/** центрирование основного контента страницы сайта **/
Content.editSiteLayer('layer');
/** отслеживание изменения размера окна браузера **/
window.onresize = function() {
/** центрирование основного контента страницы сайта **/
Content.editSiteLayer('layer');
/** центрирование облачного контента, если он открыт **/
if (ContentShow) {
Content.editSiteLayer('cloud-layer');
}
};
/** отслеживание нажатия клавиш на клавиатуре **/
window.onkeydown = function(event) {
/** если облачный контент открыт **/
if (ContentShow) {
/** кроссбраузерное событие **/
event = event || window.event;
switch(event.keyCode) {
case 27: // клавиша "Escape"
Content.cloudClose();
break;
case 37: // клавиша "Стрелка влево" (если для нее задан ID контента в массиве)
if (ContentPointPrev !== null){Content.cloudShow(ContentPointPrev);}
break;
case 39: // клавиша "Стрелка вправо" (если для нее задан ID контента в массиве)
if (ContentPointNext !== null){Content.cloudShow(ContentPointNext);}
break;
}
}
};
}
Данный метод необходимо прописать в самом конце страницы перед закрытием тега BODY:
Некоторые поинтересуются, почему именно так, и почему нельзя повесить его на тег body в виде BODY onload=«Content.init();» либо заюзать конструкцию window.onload() Но в этом случае наш объект не начнет работать, пока не загрузится вся страница до конца. Если же у вас на странице много рекламы, то это тем более неприятно. Да и резкий скачек страницы влево на 9px при ее центрировании немного будет надоедать.
В общем, с основными задачами мы с вами справились. Еще раз ДЕМО ПРИМЕР.
Спасибо за то, что дочитали статью до конца.
Скачать целиком все CSS, JavaScript файлы и html из демо примера можно в этом архиве.
С удовольствием отвечу на все ваши вопросы, а так же выслушаю любую критику.
Автор: DimaLondon