Me dedico al diseño y programación de sitios web. Casi todos los diseños que he hecho tienen ventanas modales. Por lo general, se trata de formularios de pedidos de llamadas en las páginas de destino, notificaciones sobre la finalización de algunos procesos o mensajes de error.
El diseño de tales ventanas parece al principio una tarea sencilla. Los modales se pueden crear incluso sin la ayuda de JS usando solo CSS, pero en la práctica resultan ser inconvenientes y, debido a pequeños defectos, los modales molestan a los visitantes del sitio web.
Como resultado, fue concebido para hacer mi propia soluciĂłn simple.
En términos generales, hay varios scripts listos para usar, bibliotecas de JavaScript que implementan la funcionalidad de las ventanas modales, por ejemplo:
- Modal ártico,
- jquery-modal,
- iziModal,
- Micromodal.js,
- tingle.js,
- Bootstrap Modal (de la biblioteca Bootstrap), etc.
(el artĂculo no considera soluciones basadas en frameworks Frontend)
Usé algunos de ellos yo mismo, pero casi todos encontraron algunos defectos. Algunos de ellos requieren que se incluya la biblioteca jQuery, que no está disponible en todos los proyectos. Para desarrollar su solución, primero debe determinar los requisitos.
? , «, » , - NikoX «arcticModal — jQuery- ».
, ?
- , , .
- . / .
- .
- . data-, .
- – .
- , .
- IE11+
.
1. HTML CSS
1.1.
? : HTML . / CSS.
HTML ( «hystmodal»):
<div class="hystmodal" id="myModal">
<div class="hystmodal__window">
<button data-hystclose class="hystmodal__close">Close</button>
.
<img src="img/photo.jpg" alt=" " />
</div>
</div>
, </body>
(.hystmodal
). . id ( #myModal
) ( ).
, .hystmodal
. , CSS top, bottom, left right .
.hystmodal {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
overflow: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
display: flex;
flex-flow: column nowrap;
justify-content: center; /* . */
align-items: center;
z-index: 99;
/*
*/
padding:30px 0;
}
:
- ,
.hystmodal
flex- . - ,
overflow-y: auto
, . , ( Safari)-webkit-overflow-scrolling: touch
, .
.
.hystmodal__window {
background: #fff;
/* 600px
*/
width: 600px;
max-width: 100%;
/* */
transition: transform 0.15s ease 0s, opacity 0.15s ease 0s;
transform: scale(1);
}
.
â„–1. , .
- justify-content: center
. ( ), . stackoverflow. – justify-content: flex-start
, margin:auto
. .
â„–2. ie-11 , .
: flex-shrink:0
– .
â„–3. Chrome (.. padding-bottom ).
, :
-
::after
padding - .
. .hystmodal__wrap
. â„–1, padding margin-top margin-top .hystmodal__window
.
html:
<div class="hystmodal" id="myModal" aria-hidden="true" >
<div class="hystmodal__wrap">
<div class="hystmodal__window" role="dialog" aria-modal="true" >
<button data-hystclose class="hystmodal__close">Close</button>
<h1> </h1>
<p> ...</p>
<img src="img/photo.jpg" alt="" width="400" />
<p> ...</p>
</div>
</div>
</div>
aria role .
CSS .
.hystmodal__wrap {
flex-shrink: 0;
flex-grow: 0;
width: 100%;
min-height: 100%;
margin: auto;
display: flex;
flex-flow: column nowrap;
align-items: center;
justify-content: center;
}
.hystmodal__window {
margin: 50px 0;
flex-shrink: 0;
flex-grow: 0;
background: #fff;
width: 600px;
max-width: 100%;
overflow: visible;
transition: transform 0.2s ease 0s, opacity 0.2s ease 0s;
transform: scale(0.9);
opacity: 0;
}
1.2
. , display none flex.
, display . , transition, .
visibility:hidden
. , .
– . , visibility:hidden
, - aria-hidden="true"
.
:
.hystmodal--active{
visibility: visible;
}
.hystmodal--active .hystmodal__window{
transform: scale(1);
opacity: 1;
}
1.3
, html- . .hystmodal , ( opacity) . , .
.hysymodal__shadow
</body>
. , , js .
:
.hystmodal__shadow{
position: fixed;
border:none;
display: block;
width: 100%;
top: 0;
bottom: 0;
right: 0;
left: 0;
overflow: hidden;
pointer-events: none;
z-index: 98;
opacity: 0;
transition: opacity 0.15s ease;
background-color: black;
}
/* */
.hystmodal__shadow--show{
pointer-events: auto;
opacity: 0.6;
}
1.4
, , .
— overflow:hidden body html, . :
â„–4. Safari iOS , html body overflow:hidden
.
, (touchmove, touchend touchsart) js :
targetElement.ontouchend = (e) => {
e.preventDefault();
};
, , . js, , .
ps: scroll-lock, , .
– CSS. , <html>
.hystmodal__opened
:
.hystmodal__opened {
position: fixed;
right: 0;
left: 0;
overflow: hidden;
}
position:fixed
, safari, :
â„–5. / .
, - position, .
, JS ():
:
// html
let html = document.documentElement;
// :
let scrollPosition = window.pageYOffset;
// top html
html.style.top = -scrollPosition + "px";
html.classList.add("hystmodal__opened");
:
html.classList.remove("hystmodal__opened");
//
window.scrollTo(0, scrollPosition);
html.style.top = "";
, JavaScript .
2. JavaScript
2.2
IE11 2 :
- ES5, , .
- ES6, Babel, .
, .
.
HystModal
. , .
class HystModal{
/**
* ,
* js- .
* props
*/
constructor(props){
/**
*
*
* Object.assign
*/
let defaultConfig = {
linkAttributeName: 'data-hystmodal',
// ...
}
this.config = Object.assign(defaultConfig, props);
//
this.init();
}
/**
* _shadow div
* . , ..
* ,
*
*/
static _shadow = false;
init(){
/**
* , ...
*/
this.isOpened = false; //
this.openedWindow = false; // .hystmodal
this._modalBlock = false; // .hystmodal__window
this.starter = false, // ""
// ( )
this._nextWindows = false; // .hystmodal
this._scrollPosition = 0; // (. )
/**
* ...
*/
// body
if(!HystModal._shadow){
HystModal._shadow = document.createElement('div');
HystModal._shadow.classList.add('hystmodal__shadow');
document.body.appendChild(HystModal._shadow);
}
// . .
this.eventsFeeler();
}
eventsFeeler(){
/**
* data-
* - this.config.linkAttributeName
*
* ,
* html
*
*/
document.addEventListener("click", function (e) {
/**
* ,
*
*/
const clickedlink = e.target.closest("[" + this.config.linkAttributeName + "]");
/**
* ,
* ,
* _nextWindows _starter
* open (. )
*/
if (clickedlink) {
e.preventDefault();
this.starter = clickedlink;
let targetSelector = this.starter.getAttribute(this.config.linkAttributeName);
this._nextWindows = document.querySelector(targetSelector);
this.open();
return;
}
/**
* data- data-hystclose,
*
*/
if (e.target.closest('[data-hystclose]')) {
this.close();
return;
}
}.bind(this));
/** , this
* .
* this
* , .bind().
*/
// escape tab
window.addEventListener("keydown", function (e) {
// escape
if (e.which == 27 && this.isOpened) {
e.preventDefault();
this.close();
return;
}
/** Tab
*
* ( )
*/
if (e.which == 9 && this.isOpened) {
this.focusCatcher(e);
return;
}
}.bind(this));
}
open(selector){
this.openedWindow = this._nextWindows;
this._modalBlock = this.openedWindow.querySelector('.hystmodal__window');
/**
* /
* this.isOpened
*/
this._bodyScrollControl();
HystModal._shadow.classList.add("hystmodal__shadow--show");
this.openedWindow.classList.add("hystmodal--active");
this.openedWindow.setAttribute('aria-hidden', 'false');
this.focusContol(); // (. )
this.isOpened = true;
}
close(){
/**
* .
* .
*/
if (!this.isOpened) {
return;
}
this.openedWindow.classList.remove("hystmodal--active");
HystModal._shadow.classList.remove("hystmodal__shadow--show");
this.openedWindow.setAttribute('aria-hidden', 'true');
//
this.focusContol();
//
this._bodyScrollControl();
this.isOpened = false;
}
_bodyScrollControl(){
let html = document.documentElement;
if (this.isOpened === true) {
//
html.classList.remove("hystmodal__opened");
html.style.marginRight = "";
window.scrollTo(0, this._scrollPosition);
html.style.top = "";
return;
}
//
this._scrollPosition = window.pageYOffset;
html.style.top = -this._scrollPosition + "px";
html.classList.add("hystmodal__opened");
}
}
, HystModal
. , :
const myModal = new HystModal({
linkAttributeName: 'data-hystmodal',
});
/ data-hystmodal, : <a href="#" data-hystmodal="#myModal"> </a>
. :
â„–6: ( ), / , .
– . , html, .
. , (, Chrome Android). .
_bodyScrollControl()
//
let marginSize = window.innerWidth - html.clientWidth;
// ( html)
if (marginSize) {
html.style.marginRight = marginSize + "px";
}
//
html.style.marginRight = "";
close()
? , CSS , .
â„–7. , visibility:hidden
.
: visibility:hidden
. , , , , .
- CSS-
.hystmodal—moved
-.hystmodal--active
.hystmodal--moved{
visibility: visible;
}
- «transitionend» .
`.hystmodal—active
, css-. , «transitionend», .
: :
close(){
if (!this.isOpened) {
return;
}
this.openedWindow.classList.add("hystmodal--moved");
this.openedWindow.addEventListener("transitionend", this._closeAfterTransition);
this.openedWindow.classList.remove("hystmodal--active");
}
_closeAfterTransition(){
this.openedWindow.classList.remove("hystmodal--moved");
this.openedWindow.removeEventListener("transitionend", this._closeAfterTransition);
HystModal._shadow.classList.remove("hystmodal__shadow--show");
this.openedWindow.setAttribute('aria-hidden', 'true');
this.focusContol();
this._bodyScrollControl();
this.isOpened = false;
}
, _closeAfterTransition()
. , transitionend , removeEventListener , .
, , this._closeAfterTransition()
.
, addEventListener, this
, , this.
//
this._closeAfterTransition = this._closeAfterTransition.bind(this)
2.2
– .hystmodal__wrap
. .hystmodal__wrap
:
document.addEventListener("click", function (e) {
const wrap = e.target.classList.contains('hystmodal__wrap');
if(!wrap) return;
e.preventDefault();
this.close();
}.bind(this));
, .
â„–8. , ( ), .
, . , , . , .
, , click , .hystmodal__wrap
.
html, div .hystmodal__window
. div .
addEventListener : mousedown mouseup .hystmodal__wrap
. eventsFeeler()
document.addEventListener('mousedown', function (e) {
/**
* .hystmodal__wrap,
* this._overlayChecker
*/
if (!e.target.classList.contains('hystmodal__wrap')) return;
this._overlayChecker = true;
}.bind(this));
document.addEventListener('mouseup', function (e) {
/**
* .hystmodal__wrap,
* ,
* this._overlayChecker
*/
if (this._overlayChecker && e.target.classList.contains('hystmodal__wrap')) {
e.preventDefault();
!this._overlayChecker;
this.close();
return;
}
this._overlayChecker = false;
}.bind(this));
2.3
: focusContol()
, focusCatcher(event)
.
js- «Micromodal» (Indrashish Ghosh). :
1. css ( init()):
// init
this._focusElements = [
'a[href]',
'area[href]',
'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
'select:not([disabled]):not([aria-hidden])',
'textarea:not([disabled]):not([aria-hidden])',
'button:not([disabled]):not([aria-hidden])',
'iframe',
'object',
'embed',
'[contenteditable]',
'[tabindex]:not([tabindex^="-"])'
];
2. focusContol()
, . – this.starter
:
focusContol(){
/**
* , ,
* . .
*/
const nodes = this.openedWindow.querySelectorAll(this._focusElements);
if (this.isOpened && this.starter) {
this.starter.focus();
} else {
if (nodes.length) nodes[0].focus();
}
}
3. focusCatcher()
. , , ( Tab Shift+Tab ).
focusCatcher:
focusCatcher(e){
/** TAB
* .
*/
//
const nodes = this.openedWindow.querySelectorAll(this._focusElements);
//
const nodesArray = Array.prototype.slice.call(nodes);
// ,
if (!this.openedWindow.contains(document.activeElement)) {
nodesArray[0].focus();
e.preventDefault();
} else {
const focusedItemIndex = nodesArray.indexOf(document.activeElement)
if (e.shiftKey && focusedItemIndex === 0) {
//
focusableNodes[nodesArray.length - 1].focus();
}
if (!e.shiftKey && focusedItemIndex === nodesArray.length - 1) {
//
nodesArray[0].focus();
e.preventDefault();
}
}
}
, :
â„–9. IE11 Element.closest()
Object.assign()
.
Element.closest, closest matches MDN.
, webpack, element-closest-polyfill .
Object.assign
, babel- @babel/plugin-transform-object-assign
3.
, , hystModal MIT-. 3 gzip. .
hystModal, :
- (/ , , )
- ( ( ))
- - , ( ).
- - CSS
- CSS JS Webpack.