發頁面上某個元素或者達到某個條件時,頁面彈出模態框的場景應該是很常見的了,特別是在屏幕較小的移動端,例如下面這種:
對於這個效果,之前一直都沒怎么在意探究過,因為覺得應該沒什么好弄的,直到,我接到了一個包含此效果的需求之后,我才知道什么叫眼高手低,還是太年輕。
body: overflow:hidden
第一次嘗試這個效果的時候,我稍稍思考了一下,覺得給 body
加個 overflow:hidden;height: 100vh;
的樣式應該就可以了,所以就這么寫了。
DOM
如下:
// HTML
<div class="box"> <div class="box1"> </div> </div>
當彈出層出現時,給 body
設置樣式:
document.body.style.height = '100%' document.body.style.overflow = 'hidden'
寫完后順便在我的幾個瀏覽器上試了一下,桌面瀏覽器模擬移動端的那種,效果杠杠滴,和我預想的一樣,沒毛病。
but
,哪有這么簡單的事情?
桌面瀏覽器是 ok
了,但是這個項目的主要場景是 移動端,所以要通過真正的移動端瀏覽器才行,桌面瀏覽器模擬的可不算數,結果移動端瀏覽器中,除了 移動chrome
之外,全跪了,給 body
加上那兩個樣式和沒加的效果是一樣的,背景層改怎么滾動還怎么滾動。
body+html: overflow:hidden
百思不得其解之下,只好跑到網上找了一圈,結果真讓我又找到了一個答案,說是僅僅給 body
設置 overflow
是不行的,還必須同時給 html
節點也加上這個樣式才行,於是就試了一下。
document.documentElement.style.height = '100%' document.documentElement.style.overflow = 'hidden' document.body.style.height = '100%' document.body.style.overflow = 'hidden'
桌面瀏覽器模擬移動端測試通過,之前跪了的移動瀏覽器現在也都 ok
了,給這兩個元素加上上述樣式后,彈出層背景 body
確定是不會滾動了。
but
,又出現了另外一個問題,當將頁面往下滾動一段距離,也就是說 document.body.scrollTop
大於 0
時,再顯示彈出層,增加上述四行代碼時,頁面自動滾到了最頂部,也就是說瀏覽器像是自動執行了這一行代碼 document.body.scrollTop=0
。
仔細想想也是,之前頁面是超出一個屏幕高度的,所以可以滾動,但是現在你把頁面高度設為一個屏幕高度 100%
,並且 overflow:hidden
,那么根據 overflow:hidden
的特性,瀏覽器肯定是要從頁面的頭部開始截取一個屏幕的高度,剩下的再 hidden
。
如果彈出層時,背景是完全看不到的,一片漆黑,也就是類似 rgba(0,0,0,1)
,而不是半透明 rgba(0,0,0,.6)
,那么實際無傷大雅,也就是一行代碼的事情。
在彈出層彈出之前,先保存此事頁面的 scrollTop
,然后在彈出層關閉的時候,再將頁面的 scrollTop
設定到之前保存的那個位置,所以這樣最起碼看起來背景是沒變的,就像下面這樣:
var box1 = $('.box1') var body= document.body var scrolltop body.addEventListener('click', function() { if (box1.className.indexOf('hidden') !== -1) { // 保存頁面滾動到的位置 scrolltop = document.body.scrollTop body.style.height = '100%' body.style.overflow = 'hidden' document.documentElement.style.height = '100%' document.documentElement.style.overflow = 'hidden' // 顯示彈出層 box1.className ='box1 show' } else { // 隱藏彈出層 box1.className ='box1 hidden' body.style.height = 'auto' body.style.overflow = 'visible' document.documentElement.style.height = 'auto' document.documentElement.style.overflow = 'visible' // 恢復頁面滾動到的位置 document.body.scrollTop = scrolltop } })
如果背景層是可見的呢,只要用戶不瞎,肯定能看到頁面發生跳動了啊。
JS控制
- 區別對待
看來只通過 css
來完成這個效果是有些難度了,於是將主意打到了 js
上,如下:
box1.addEventListener('touchmove', function(e){ e.preventDefault() })
直接禁止彈出層的掉滾動事件,因為彈出層是滿屏覆蓋在 頁面上的,而且這個事件也沒有 點透
,所以確實是達到了禁止背景 頁面滾動的效果。
but
,背景元素的滾動是禁止掉了,但這種禁止幾乎是把頁面上所有元素的滾動事件都禁止掉了,如果在彈出層元素 box1
中存在可以滾動的元素,那么同樣也會被禁止滾動,這可不是我們想要的,所以必須要把彈出層內的元素排除在外才行。
彈出層內可滾動區域包括可滾動的頂級元素,以及此元素下所有的子元素,所以只要判斷當前正在滾動的區域是此區域內的元素,則允許滾動,所以這里需要判斷當前 touch
的元素是不是彈出層內可以滾動的元素,以及是不是其子元素,判斷是否為其子元素只需要一個循環遞歸即可,例如以下代碼:
function getRecursiveEle(ele, parentClassName) { if (ele.className.indexOf(parentClassName) !== -1) { return ele } else { if (ele.nodeName.toLowerCase() === 'body') { return null } ele = ele.parentNode return getRecursiveEle(ele, parentClassName) } }
調用此函數,傳入 e.target
以及彈出層可以滾動的元素類名即可,返 回 true
,則表明是可以滾動的元素。
but
,雖然你直接禁止了彈出層可滾動元素其外的元素滾動,然而同時又允許彈出層內可滾動元素滾動,那么當將滾動元素滾動到頭的時候,背景還是會滾動。
所以,還需要加一個判斷,當滾動元素滾動到頭或者尾部的時候,再禁止所有元素滾動,這里的滾動到頭包括兩種情況,到頭和到尾。
// 滾動到頭的情況,其中touchstartY 為開始滾動時接觸點的 `pageY`
if(modal.scrollTop <= 0) { e.targetTouches[0].pageY > touchstartY && e.preventDefault() }
// 滾動到尾的情況,其中touchstartY 為開始滾動時接觸點的 `pageY`,itemH為可滾動元素框內部的子元素總高度,+2是因為邊界問題 (itemH - modal.offsetHeight < modal.scrollTop + 2) && (e.targetTouches[0].pageY < touchstartY) && e.preventDefault()
這樣的話,問題大體上解決了,但還有點小問題,在有的瀏覽器上,當可滾動元素滾動到頭的時候,背景依舊還是會稍微滾動一點距離,不太完美。
- 另想他法
依舊讓覆蓋整個屏幕的彈出層禁止滾動,彈出層內部可滾動元素的滾動通過 js
來控制,例如使用 translate
控制上下滾動距離。
// touchstartY 為touchstart事件發生時的 e.targetTouches[0].pageY var translateEndY = 0, translateEndYTemp = 0 box1.addEventListener('touchmove', function(e) { // 禁止默認滾動 e.preventDefault() translateEndYTemp = e.targetTouches[0].pageY-touchstartY + translateEndY // 通過改變 translate來滾動元素 $('.item').style.transform = 'translate(0, '+translateEndYTemp+'px)' }) // 緩存下每次滾動過的距離 box1.addEventListener('touchend', function(e) { translateEndY = translateEndYTemp })
嗯,這樣就差不多了,不過因為滾動時通過 translate
實現的,所以滾動元素是不受父元素約束的,也就是說滾動元素會滾過界,這個很好解決,在 touchend
的時候,判斷一下有沒有過界,如果過界了反彈回來就行