О модальных окнах написано уже, наверное, тонны литературы, но на написание этого топика меня сподвигла вот эта статья на хабре. В ней осталось много недосказанного, в том числе горизонтальные скачки как страницы, так и модального окна.
Конечно, можно было бы просто отписаться в комментарих, мол, сделайте так-то и так-то, и все будет тип-топ. Но мой комментарий разросся до размеров новой статьи, с наглядными примерами и комментариями.
Кому стало интересно — добро пожаловать под хабракат!
Прямо скажу, что данный вид просмотра контента на странице называть «модальным окном» у меня язык не поворачивается. Все-таки это немного разные вещи. Поэтому я буду называть его «облачный контент». Думаю, это наиболее правильное определение для подобного вида отображения контента.
Итак, наши основные задачи:
1) умудриться зафиксировать страницу так, чтобы при горячем изменении ее высоты она не «прыгала» вправо-влево.
2) открыть облачное окно и отцентрировать его так, чтобы и при изменении и его высоты контент на странице оставался на месте.
3) добиться вертикальной прокрутки «облачного окна» при помощи клавиш на клавиатуре «Стрелки вверх и вниз».
4) ранее открытые окна кешировать в DOM, дабы не производить сложных действий и вычислений или не делать лишний запрос к серверу.
5) подключить горячие навигационные клавиши для максимально удобной навигации конечного пользователя (стрелки клавиатуры Вправо/Влево для перемещениями между окнами и клавиша Escape для закрытия окна).
Пункты 4 и 5 и будут плюшками.
Ко всему прочему, мы не будем использовать никаких фреймворков, и будем писать на нативном JS. Забегая наперед, скажу, что JS код без комментариев в несжатом виде занимает всего 5 кб.
Собственно, начнем.
Сразу покажу готовый наглядный пример, чтобы при чтении данной статьи у вас было с чем сравнить и посмотреть, о чем конкретно идет речь.
ДЕМО ПРИМЕР и подключенный к странице JS файл (JS может открыться «побитым», поэтому можете сразу скачать архив целиком).
Для начала мы определим функцию elemID(); Так как конструкция document.getElementById встречается в коде довольно часто, проще укоротить эту запись при помощи подобной функции, что позволяет значительно уменьшить общий вес JS файла. Это особо важно при массивных файлах с большим объемом кода.
Заводим объект, с которым будем впоследствии работать:
Первое, с чего мы начнем — это фиксация страницы. Для этого мы весь контент страницы обернем в блок с id=«layer»
Готово. Теперь для него пишем метод, центрирующий этот блок:
Написали и временно забыли о нем (в конце мы обязательно подключим его).
Вторая наша задача — это создать и открыть «облачное окно».
Открывать облачное окно мы будем так:
Блок «cloud» выполняет роль некой защитной маски и отделяет контент сайта от облачного окна, при этом и делая его «облачным». Его CSS свойства:
Блок «cloud-layer» является «родителем» для всех DOM элементов с облачным контентом. Фиксируется он по центру страницы и ему задается свойство position:relative; (именно оно позволяет скроллить окно клавишами клавиатуры).
Его CSS свойства:
Блок «cloud-contaner» — это и есть контейнер для нашего облачного контента. Почему именно в теге EM? Впоследствии мы будем пробегаться по DOM в поиске актуального блока и с целью скрыть все уже существующие. Поиск по тегу для нас подходит как нельзя кстати.
Чтобы понять реальную задачу — представим, что нам хочется просмотреть все личные фотографии пользователя на нашем сайте. Обычно делается так: при первом нажатии на фотографию делается http-запрос к серверу для получения массива всех фотографий, их уникальных ID, описанием и прочей информацией.
Для примера я не буду этого делать, а заранее руками прописал этот массив и приготовил его к работе:
Теперь при открытии окна мы опираемся на уникальный ID фотографии, перебираем массив, выбираем из него нужную информацию по текущему фото, а так же ID предыдущего и следующего фото для навигации. Параллельно пробегаемся по DOM и проверяем, существует ли в DOM актуальный контент. Если существует — открываем его, если нет — генерируем новый блок и вставляем его в облако. Но для начала проверяем, есть ли блок с облаком в DOM или еще нет. Если еще нет и это первое открытие облака — генерация облака, а в нем — генерация родителя облачного контента.
Выше я изпользовал некоторые значения массива «lorem» — в нем я заранее заготовил очень длинный текст для наглядного удлинения страницы, дабы показать, что окно зафиксировано.
При открытии окна заюзан метод Content.editSiteLayer('cloud-layer'); — уже ранее описанным методом мы центрируем облачное окно по центру страницы, не позаоляя ему скакать вправо-влево при появлении полосы прокрутки.
При закрытии окна используется метод Content.cloudClose(); Вот он:
И последняя наша задача — это подключение горячих клавиш. Для этого мы создадим метод Content.init(), который будет обрабатывать горячие клавиши и при загрузке страницы сразу центрировать ее в окне обозревателя (то, о чем я писал в самом начале статьи):
Данный метод необходимо прописать в самом конце страницы перед закрытием тега <BODY>:
Некоторые поинтересуются, почему именно так, и почему нельзя повесить его на тег body в виде
<BODY onload=«Content.init();»> либо заюзать конструкцию window.onload() Но в этом случае наш объект не начнет работать, пока не загрузится вся страница до конца. Если же у вас на странице много рекламы, то это тем более неприятно. Да и резкий скачек страницы влево на 9px при ее центрировании немного будет надоедать.
В общем, с основными задачами мы с вами справились. Еще раз ДЕМО ПРИМЕР.
Тестировалось в браузерах Opera 12.00, Chrome 21 и выше, IE 8 и 9, Safari 5.0 и Yandex.browser 1.0
К сожалению, в Firefox протестировать нет возможности.
Спасибо за то, что дочитали статью до конца.
Скачать целиком все CSS, JavaScript файлы и html из демо примера можно в этом архиве.
В архиве находятся 2 JS файла:
page.js — файл с подробными комментариями
js.js — тот же файл, но уже без комментариев
С удовольствием отвечу на все ваши вопросы, а так же выслушаю любую критику.
Конечно, можно было бы просто отписаться в комментарих, мол, сделайте так-то и так-то, и все будет тип-топ. Но мой комментарий разросся до размеров новой статьи, с наглядными примерами и комментариями.
Кому стало интересно — добро пожаловать под хабракат!
Прямо скажу, что данный вид просмотра контента на странице называть «модальным окном» у меня язык не поворачивается. Все-таки это немного разные вещи. Поэтому я буду называть его «облачный контент». Думаю, это наиболее правильное определение для подобного вида отображения контента.
Итак, наши основные задачи:
1) умудриться зафиксировать страницу так, чтобы при горячем изменении ее высоты она не «прыгала» вправо-влево.
2) открыть облачное окно и отцентрировать его так, чтобы и при изменении и его высоты контент на странице оставался на месте.
3) добиться вертикальной прокрутки «облачного окна» при помощи клавиш на клавиатуре «Стрелки вверх и вниз».
4) ранее открытые окна кешировать в DOM, дабы не производить сложных действий и вычислений или не делать лишний запрос к серверу.
5) подключить горячие навигационные клавиши для максимально удобной навигации конечного пользователя (стрелки клавиатуры Вправо/Влево для перемещениями между окнами и клавиша Escape для закрытия окна).
Пункты 4 и 5 и будут плюшками.
Ко всему прочему, мы не будем использовать никаких фреймворков, и будем писать на нативном JS. Забегая наперед, скажу, что JS код без комментариев в несжатом виде занимает всего 5 кб.
Собственно, начнем.
Сразу покажу готовый наглядный пример, чтобы при чтении данной статьи у вас было с чем сравнить и посмотреть, о чем конкретно идет речь.
ДЕМО ПРИМЕР и подключенный к странице JS файл (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>:
<script type="text/javascript" language="javascript">Content.init();</script>
Некоторые поинтересуются, почему именно так, и почему нельзя повесить его на тег body в виде
<BODY onload=«Content.init();»> либо заюзать конструкцию window.onload() Но в этом случае наш объект не начнет работать, пока не загрузится вся страница до конца. Если же у вас на странице много рекламы, то это тем более неприятно. Да и резкий скачек страницы влево на 9px при ее центрировании немного будет надоедать.
В общем, с основными задачами мы с вами справились. Еще раз ДЕМО ПРИМЕР.
Тестировалось в браузерах Opera 12.00, Chrome 21 и выше, IE 8 и 9, Safari 5.0 и Yandex.browser 1.0
К сожалению, в Firefox протестировать нет возможности.
Спасибо за то, что дочитали статью до конца.
Скачать целиком все CSS, JavaScript файлы и html из демо примера можно в этом архиве.
В архиве находятся 2 JS файла:
page.js — файл с подробными комментариями
js.js — тот же файл, но уже без комментариев
С удовольствием отвечу на все ваши вопросы, а так же выслушаю любую критику.
