1. 程式人生 > >Bootstrap簡單認識之Modal元件

Bootstrap簡單認識之Modal元件

一、簡介

二、樣式

此版本的Bootstrap大量使用了flex佈局

  • .modal-content 中,使用了 flex-direction: column,使得 .modal-header、.modal-body、.modal-footer縱向排列
  • .modal-header 中,使用了 display: flex; align-items:center 使得內容縱向居中,使用了 justify-content:space-between 使得兩端對齊,專案之間的間隔都相等(主要就是title以及關閉按鈕)
  • .modal-body 中,作為flex item使用了屬性 flex:1 1 auto
    ,第一個值為 flex-grow,在 modal-content 中佔據足夠大的空間(因為header和footer的flex-grow都為預設值0); 第二個值為 flex-shrink,預設都為1;第三個值為 flex-basis,定義在分配多餘空間之前專案佔據主軸(這裡的 .modal-content 的方向是 column,所以主軸是縱向)的空間
  • .modal-footer 中,使用了 display: flex;align-items: center; 使得內容縱向居中,使用了 justify-content: flex-end; 使得每個flex item 向右靠攏(預設放在footer的是一些按鈕啥的)

三、指令碼

Modal元件的指令碼一般用於控制Modal的顯示與隱藏,所以可以想象主要就是show和hide函式
下面是Modal元件的程式碼梗概:

class Modal {

  constructor(element, config) {
    // 配置屬性,包括backdrop(是否有背景陰影層),keyboard(是否可用鍵盤控制esc鍵),show(是否需要在初始化的同時彈出Modal),focus(讓_element元素focus?不理解)
    this._config              = this._getConfig(config)
    // 這個元素並不是那個彈出來的框框, 而是modal-dialog類元素的更外一層, 即Modal的最外層(注意,也不是背景層, 背景層是動態新增在body底部的元素)
this._element = element // 這個就是指彈出來的框框元素 this._dialog = $(element).find(Selector.DIALOG)[0] // 背景陰影層元素 this._backdrop = null // 是否彈框狀態 this._isShown = false // 原頁面是否有滾動條 this._isBodyOverflowing = false // 是否忽略_element的click事件(hide Modal) this._ignoreBackdropClick = false // 是否正在執行動畫 this._isTransitioning = false // 頁面原始body的padding-right值 this._originalBodyPadding = 0 // 瀏覽器滾動條寬度 this._scrollbarWidth = 0 } // public // 彈出或是隱藏Modal toggle(relatedTarget) {} // 彈出 show(relatedTarget) {} // 隱藏 hide(event) {} // static static _jQueryInterface(config, relatedTarget) { return this.each(function () { let data = $(this).data(DATA_KEY) // 初始化的時候同時取預設屬性值和按鈕上的data-*屬性和Modal元素的data-*屬性 const _config = ... if (!data) data = new Modal(this, _config) $(this).data(DATA_KEY, data) } // 如果是命令,那麼執行對應的函式 if (typeof config === 'string') { if (data[config] === undefined) { throw new Error(`No method named "${config}"`) } data[config](relatedTarget) } else if (_config.show) { // 如果配置了 data-show="true",那麼初始化的同時會開啟Modal data.show(relatedTarget) } }) } } // 觸發click事件的按鈕 $(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { // target表示按鈕控制的Modal元素 let target const selector = Util.getSelectorFromElement(this) if (selector) { target = $(selector)[0] } // 要麼初始化,要麼toggle const config = $(target).data(DATA_KEY) ? 'toggle' : $.extend({}, $(target).data(), $(this).data()) // Modal的show事件觸發 const $target = $(target).one(Event.SHOW, (showEvent) => { if (showEvent.isDefaultPrevented()) { // only register focus restorer if modal will actually get shown return } // Modal在hidden事件觸發後(Modal關閉後),觸發此Modal出現的按鈕會被focus $target.one(Event.HIDDEN, () => { if ($(this).is(':visible')) { this.focus() } }) }) Modal._jQueryInterface.call($(target), config, this) })

上述依舊是老套路,在Modal元素上依附例項,可以通過點選按鈕初始化例項或是js呼叫來初始化例項,沒有什麼特別的地方

function show

show 函式用於彈出Modal,這裡是貼出其執行的流程附加部分,這樣可以更加便於理解整個執行的過程。流程如下:

  1. 如果有fade類, 設定_isTransitioning狀態為true, 表明要執行動畫
  2. 觸發 show.bs.modal 事件,如果該事件回撥過程中有執行preventdefault,那麼此show函式不再繼續執行
  3. 修改 isShown 狀態為true,為將來註冊事件做準備,表明將來是 show
  4. 通過document.body.clientWidth < window.innerWidth判斷y軸是否有滾動條,判斷結果儲存在_isBodyOverflowing屬性中,且通過新增刪除一個 overflow:scroll的元素, 獲取此瀏覽器滾動條的寬度 offsetWidth - clientWidth

    _getScrollbarWidth() {
        const scrollDiv = document.createElement('div')
        scrollDiv.className = ClassName.SCROLLBAR_MEASURER
        document.body.appendChild(scrollDiv)
        const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth
        document.body.removeChild(scrollDiv)
        return scrollbarWidth
    }
  5. 如果頁面存在滾動條,在下一步為body新增 overflow:hidden 屬性後,頁面缺失滾動條後變寬了,佈局會變化,為了防止變化這一步添加了padding-right = 滾動條寬度,這樣頁面就不會有變化
  6. 為body新增 overflow:hidden 屬性(為body新增open類)
  7. 註冊鍵盤ESC鍵觸事件(關閉Modal,即呼叫hide函式)
  8. 隨著瀏覽器視窗的變化,動態調整this._element的padding-left或是padding-right使得頁面不發生調整

    _adjustDialog() {
      // Modal彈出框的scrollHeight一般是與documentElement.clientHeight相同的,但是當documentElement.clientHeight過小時,Modal的scrollHeight會不變從而大於documentElement.clientHeight
      // 這個時候雖然body是overflow:hidden的,但Modal最外層是寬高100%且overflow-y:auto的,所以頁面就會顯示滾動條
      // isModalOverflowing表示此時是否為Modal顯示滾動條
      const isModalOverflowing =
        this._element.scrollHeight > document.documentElement.clientHeight
    
      // 原來頁面是沒有滾動條的
      // 這種情況是考慮突然出現滾動條
      // 現在要為Modal新增滾動條,如果突然加上滾動條(相當於突然新增padding-right:17px)頁面會發生變化,
      // 但是如果添加了padding-left:17px,此時和隱形的padding-right:17px(即滾動條)相抵,頁面就不發生變化了
      // 實驗中發現:突然變小瀏覽器視窗的高度,Modal位置不會發生變化,但是高度再一次變大後Modal會移動(因為bootstrap沒有移除padding-left:17px,但此時滾動條沒了相當於只變了padding-right),
      // 再次變小又會移動(因為變小之前沒有滾動條只有padding-left:17px, 而突然右邊多了個滾動條,頁面佈局當然發生變化)
      if (!this._isBodyOverflowing && isModalOverflowing) {
        this._element.style.paddingLeft = `${this._scrollbarWidth}px`
      }
    
      // 原本整個頁面body是有滾動條的(相當於padding-right:17px)
      // 這種情況是考慮突然沒有了滾動條
      // 原本是有滾動條的,但是視窗大小發生變化後突然沒了滾動條,相當於padding-right突然為0,這樣
      // 會導致頁面發生變化,此時如果加上padding-right的屬性代替之前的滾動條,那麼相當於頁面沒有變化
      if (this._isBodyOverflowing && !isModalOverflowing) {
        this._element.style.paddingRight = `${this._scrollbarWidth}px`
      }
    }
  9. 為右上角的關閉按鈕註冊關閉事件
  10. 註冊事件防止一種情況:在彈出框_dialog上滑鼠點下去了,但是在外層_element上把滑鼠鬆開來了,這種情況下即使配置屬性backdrop是true,彈出框也不會hide

     $(this._dialog).on(Event.MOUSEDOWN_DISMISS, () => {
      $(this._element).one(Event.MOUSEUP_DISMISS, (event) => {
        // 同時避免事件冒泡
        if ($(event.target).is(this._element)) {
          this._ignoreBackdropClick = true
        }
      })
    })
  11. 下面步驟即為顯示陰影層(_backdrop元素)
  12. 將帶有class為 ‘.modal-backdrop.fade’ 的div元素(即陰影層)插入body (注意:’.modal-backdrop’控制此全屏顯示 (fixed, left,right,top,bottom:0))
  13. 為最外層的_element註冊點選事件,如果配置屬性backdrop是true,那麼點選後彈出框會hide

    $(this._element).on(Event.CLICK_DISMISS, (event) => {
      // 如果此次點選,mousedown發生在_dialog,而mouseup發生在_element,從而觸發了click
      // 那麼即使配置屬性backdrop是true,也不會hide這個Modal
      if (this._ignoreBackdropClick) {
        this._ignoreBackdropClick = false
        return
      }
    
      // target一般指觸發事件的元素,currentTarget(相當於this)一般則是事件冒泡上來觸發函式的元素
      // 避免事件冒泡觸發此函式(這裡由於是包含關係,所以必須避免這種情況)
      if (event.target !== event.currentTarget) {
        return
      }
      if (this._config.backdrop === 'static') {
        this._element.focus()
      } else {
        this.hide()
      }
    })
  14. 為背景陰影層新增show類,fade+show=(opacity:0.5)
  15. 下面步驟顯示彈框(_element + _dialog)
  16. 為_element新增show類,fade + show = (opacity : 1)
  17. 觸發shown事件
  18. 執行動畫彈框或是不支援動畫直接彈框

以上為整個Show函式的執行過程,雖然整個過程為了防止頁面佈局發生變化花了很多心思,但一些情況下依舊會變化,如果有時間,我希望能夠來調整一番

function hide
hide事件與show事件恰好相反,但是要關注的細節明顯少了許多,下面直接貼上程式碼以及註釋

hide(event) {
  // 觸發hide事件
  ... ...
  // 改變_isShown狀態,應對接下來的事件處理
  this._isShown = false
  // 移除鍵盤ESC觸發的事件監聽
  this._setEscapeEvent()
  // 移除視窗變化的事件監聽
  this._setResizeEvent()
  // 最外層移除show類, 即 opacity:0
  $(this._element).removeClass(ClassName.SHOW)
  // 移除點選非_dialog元素hide Modal的事件監聽
  $(this._element).off(Event.CLICK_DISMISS)
  // 移除防止滑鼠點選極端情況的事件監聽
  $(this._dialog).off(Event.MOUSEDOWN_DISMISS)

  // 隱藏_element元素(_hideModal函式)
  this._element.style.display = 'none'
  // 觸發hidden事件
  ... ... 
}

四、細節

最後,這裡討論一下視窗變化事件( $(window).resize() )註冊的 _adjustDialog 回撥,此事件影響了彈出框相對於瀏覽器視窗發生變化時頁面做出的佈局調整情況。
主要根據兩個屬性作出相應變化:

  • _isBodyOverflowing : 原始頁面是否有滾動條,相當於是否已經為body元素設定 padding-right:?px 屬性,? 表示滾動條寬度
  • isModalOverflowing: 彈出框是否有滾動條

根據程式碼,只能處理以下兩種情況:

  • 若body標籤沒有padding-right:?px 屬性,彈出框在視窗調整後需要滾動條,為_element元素新增屬性 paddingLeft: ?px
  • 若body標籤有padding-right:?px 屬性,彈出框在視窗調整後不再需要滾動條,為_element元素新增屬性 paddingRight: ?px

五、修改

對於上述處理方式,可能是我沒能完全理解編寫的初衷,或是有誤解。但是為了達到瀏覽器高度變化不會導致彈出框發生調整的目的,我自己有了如下實現。

實現的主要思想是:
1. 當滾動條從無到有,那麼設定_element的padding-left為滾動條寬度;
2. 當滾動條從有到無,設定_element的左右padding都為0

下面是在 _showElement 函式新增的程式碼,目的是在Modal彈出後獲取當前是否顯示滾動條:

_showElement(){
    ... ...

    // 彈出動畫結束後(如果有動畫的話)的回撥函式
    const transitionComplete = () => {
        if (this._config.focus) {
          this._element.focus()
        }
        this._isTransitioning = false
        $(this._element).trigger(shownEvent)
        // 在shown鉤子觸發後,獲取當前是否顯示滾動條
        this.prevModalOverFlow = this._element.scrollHeight > document.documentElement.clientHeight;
        if(this.prevModalOverFlow){
          // 如果在彈出Modal時發現是有顯示滾動條的,那麼按照上述思想設定padding-left的值
          // 一開始就沒滾動條那初始狀態就是padding兩邊都為0
          this._element.style.paddingLeft = this._scrollbarWidth + 'px';
        }
      }
}

下面是改變的原來 _adjustDialog 函式程式碼:

_adjustDialog() {
  // 判斷此次調整是否有顯示滾動條,與之前一樣
  const isModalOverflowing =
    this._element.scrollHeight > document.documentElement.clientHeight
  // 下面程式碼也很直觀
  if(this.prevModalOverFlow && !isModalOverflowing){
    this._element.style.paddingRight = '';
    this._element.style.paddingLeft = '';
  }

  if(!this.prevModalOverFlow && isModalOverflowing){
    this._element.style.paddingLeft = this._scrollbarWidth + 'px';
    this._element.style.paddingRight = '';
  }

  this.prevModalOverFlow = isModalOverflowing
}

這樣一來,瀏覽器的高度無論如何變化,Modal都不會左右移動。