我们为网站制作模式窗口。我们关心便捷性

我从事网站布局和编程。我几乎完成的每个布局都有模态。通常,这些是登陆页面中的呼叫订单表格,有关某些过程完成的通知或错误消息。



起初,此类窗口的布局似乎很简单。即使没有CSS的JS的帮助,也可以制作模态,但是实际上它们很不方便,并且由于存在一些小缺陷,模态惹恼了站点访问者。



结果,它被认为是我自己的简单解决方案。





一般来说,有几种现成的脚本,即实现模态窗口功能的JavaScript库,例如:



  • 北极模态
  • jquery-modal,
  • iziModal,
  • Micromodal.js,
  • tingle.js,
  • Bootstrap Modal(来自Bootstrap库)等


(我们不会在本文中考虑基于Frontend框架的解决方案)



我本人使用了其中一些,但几乎所有人都发现了一些缺陷。其中一些要求包含jQuery库,但并非在所有项目中都可用。要开发解决方案,您必须首先确定要求。



? , «, » , - NikoX «arcticModal — jQuery- ».



, ?



  • , , .
  • . / .
  • .
  • . data-, .
  • – .
  • , .
  • IE11+


: , (HystModal) GitHub, +.



.



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;
}


:



  1. , .hystmodal flex- .
  2. , 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.


, GitHub, Issues . ( , , , . Instagram




All Articles