使用原生js編寫一個簡單的框選功能方法
今天我們來聊一下怎么使用原生javascript編寫一個簡單的框選功能。
需求描述
- 鼠標左鍵按下不放,移動鼠標出現(xiàn)矩形選框;
- 鼠標左鍵松開,根據(jù)上邊出現(xiàn)的矩形選框統(tǒng)計選框范圍內(nèi)的DOM元素;
嗯...上邊的功能描述看著是挺簡單的,但實現(xiàn)起來也還是會有些地方需要斟酌思考的。比如,如果我們的框選范圍不是document.body,而是某一個div里邊進行框選呢?而現(xiàn)實開發(fā)過程中,我們會遇上的應該就是第二種情況。
怎么實現(xiàn)
二話不說,咱們動手寫代碼吧!因為更好的兼容性,這里就避免了一些ES6的語法,如果是用的其他框架來寫的話,代碼上相應的也要做一些調(diào)整。
<head> <style> .fileDiv { display: inline-block; width: 100px; height: 100px; margin: 24px; background-color: blue; } </style> </head> <body> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> </body>
添加鼠標事件監(jiān)聽
由于js自身并沒有帶有鼠標點擊按住不放這樣子的事件,這里我們不僅需要檢測鼠標左鍵點擊按下,還要加一個定時器來檢測鼠標是否按住不放了。
<script> (function () { // 定時器id var mouseStopId; // 是否開啟框選功能 var mouseOn = false; // 用來存放鼠標點擊初始位置 var startX = 0; var startY = 0; // 添加鼠標按下監(jiān)聽事件 document.body.addEventListener('mousedown', function (e) { // 阻止事件冒泡 clearEventBubble(e); // 判斷是否為鼠標左鍵被按下 if (e.buttons !== 1 || e.which !== 1) return; mouseStopId = setTimeout(function () { mouseOn = true; startX = e.clientX; startY = e.clientY; }, 300); // 間隔300毫秒后執(zhí)行,判定這時候鼠標左鍵被按住不放 }); // 添加鼠標移動事件監(jiān)聽 document.body.addEventListener('mousemove', function (e) { // 如果并非框選開啟,退出 if (!mouseOn) return; // 阻止事件冒泡 clearEventBubble(e); // 處理鼠標移動 // codes }); // 添加鼠標點擊松開事件監(jiān)聽 document.body.addEventListener('mouseup', function (e) { // 阻止事件冒泡 clearEventBubble(e); // 處理鼠標點擊松開 // codes }); function clearEventBubble (e) { if (e.stopPropagation) e.stopPropagation(); else e.cancelBubble = true; if (e.preventDefault) e.preventDefault(); else e.returnValue = false; } })(); </script>
添加框選可視化元素
框選可視化元素示意圖
我們有了事件監(jiān)聽還不夠,為了更好的交互效果,我們需要一個隨時跟隨著鼠標移動的框選框元素,用于讓用戶隨時感知框選范圍。
<script> (function () { var mouseStopId; var mouseOn = false; var startX = 0; var startY = 0; document.body.addEventListener('mousedown', function (e) { clearEventBubble(e); if (e.buttons !== 1 || e.which !== 1) return; mouseStopId = setTimeout(function () { mouseOn = true; startX = e.clientX; startY = e.clientY; // 創(chuàng)建一個框選元素 var selDiv = document.createElement('div'); // 給框選元素添加css樣式,這里使用絕對定位 selDiv.style.cssText = 'position:absolute;width:0;height:0;margin:0;padding:0;border:1px dashed #eee;background-color:#aaa;z-index:1000;opacity:0.6;display:none;'; // 添加id selDiv.id = 'selectDiv'; document.body.appendChild(selDiv); // 根據(jù)起始位置,添加定位 selDiv.style.left = startX + 'px'; selDiv.style.top = startY + 'px'; }, 300); }); document.body.addEventListener('mousemove', function (e) { if (!mouseOn) return; clearEventBubble(e); // 獲取當前坐標 var _x = e.clientX; var _y = e.clientY; // 根據(jù)坐標給選框修改樣式 var selDiv = document.getElementById('selectDiv'); selDiv.style.display = 'block'; selDiv.style.left = Math.min(_x, startX) + 'px'; selDiv.style.top = Math.min(_y, startY) + 'px'; selDiv.style.width = Math.abs(_x - startX) + 'px'; selDiv.style.height = Math.abs(_y - startY) + 'px'; // 如果需要更直觀一點的話,我們還可以在這里進行對框選元素覆蓋到的元素進行修改被框選樣式的修改。 }); document.body.addEventListener('mouseup', function (e) { clearEventBubble(e); }); function clearEventBubble (e) { if (e.stopPropagation) e.stopPropagation(); else e.cancelBubble = true; if (e.preventDefault) e.preventDefault(); else e.returnValue = false; } })(); </script>
添加鼠標松開事件監(jiān)聽
元素是否被選中示意圖
我們沒有在鼠標移動的時候去實時統(tǒng)計被框選到的DOM元素,如果需要實時統(tǒng)計或者實時修改被選擇的DOM元素的樣式,以便更準確的讓用戶感知到被框選的內(nèi)容的話,可以選擇在mousemove事件里邊去實現(xiàn)以下代碼:
<script> (function () { var mouseStopId; var mouseOn = false; var startX = 0; var startY = 0; document.onmousedown = function (e) { clearEventBubble(e); if (e.buttons !== 1 || e.which !== 1) return; mouseStopId = setTimeout(function () { mouseOn = true; startX = e.clientX; startY = e.clientY; var selDiv = document.createElement('div'); selDiv.style.cssText = 'position:absolute;width:0;height:0;margin:0;padding:0;border:1px dashed #eee;background-color:#aaa;z-index:1000;opacity:0.6;display:none;'; selDiv.id = 'selectDiv'; document.body.appendChild(selDiv); selDiv.style.left = startX + 'px'; selDiv.style.top = startY + 'px'; }, 300); } document.onmousemove = function (e) { if (!mouseOn) return; clearEventBubble(e); var _x = e.clientX; var _y = e.clientY; var selDiv = document.getElementById('selectDiv'); selDiv.style.display = 'block'; selDiv.style.left = Math.min(_x, startX) + 'px'; selDiv.style.top = Math.min(_y, startY) + 'px'; selDiv.style.width = Math.abs(_x - startX) + 'px'; selDiv.style.height = Math.abs(_y - startY) + 'px'; }; // 添加鼠標松開事件監(jiān)聽 document.onmouseup = function (e) { if (!mouseOn) return; clearEventBubble(e); var selDiv = document.getElementById('selectDiv'); var fileDivs = document.getElementsByClassName('fileDiv'); var selectedEls = []; // 獲取參數(shù) var l = selDiv.offsetLeft; var t = selDiv.offsetTop; var w = selDiv.offsetWidth; var h = selDiv.offsetHeight; for (var i = 0; i < fileDivs.length; i++) { var sl = fileDivs[i].offsetWidth + fileDivs[i].offsetLeft; var st = fileDivs[i].offsetHeight + fileDivs[i].offsetTop; if (sl > l && st > t && fileDivs[i].offsetLeft < l + w && fileDivs[i].offsetTop < t + h) { // 該DOM元素被選中,進行處理 selectedEls.push(fileDivs[i]); } } // 打印被選中DOM元素 console.log(selectedEls); // 恢復參數(shù) selDiv.style.display = 'none'; mouseOn = false; }; function clearEventBubble (e) { if (e.stopPropagation) e.stopPropagation(); else e.cancelBubble = true; if (e.preventDefault) e.preventDefault(); else e.returnValue = false; } })(); </script>
這里判斷一個元素是否被選中采用的判斷條件是:
- 該DOM元素的最右邊(fileDiv[i].offsetLeft + fileDiv[i].offsetWidth)是否要比選框元素最左邊(selDiv.offsetLeft)的位置要??;
- 該DOM元素的最下邊(fileDiv[i].offsetTop + fileDiv[i].offsetHeight)是否要比選框元素的最上邊(selDiv.offsetTop)的位置要大;
- 該DOM元素的最左邊(fileDiv[i].offsetLeft)是否要比選框元素的最后邊(selDiv.offsetLeft + selDiv.offsetWidth)的位置數(shù)值要??;
- 該DOM元素的最上邊(fileDiv[i].offsetTop)是否要比選框元素的最下邊(selDiv.offsetTop + selDiv.offsetHeight)的位置數(shù)值要小;
滿足了以上四個條件,即可判別為該DOM元素被選中了。
實際應用
上邊的例子,舉得有些過于簡單了。實際的開發(fā)當中,框選的范圍往往不可能是整個document.body,而是某一個具體的有特定寬度跟高度限制的元素。這個時候,就還需要考慮這個框選容器元素造成的定位偏差,以及容器內(nèi)元素過多,出現(xiàn)滾動條的情況了。
乍一看,上邊的情況需要考慮的因素多了不少,比較容易亂。我這里采用的方法是修改坐標系的方式來實現(xiàn)上邊描述的功能。上文我們已經(jīng)實現(xiàn)了在document.body整個頁面左上角頂點作為坐標原點來實現(xiàn)框選功能,這時候我們需要修改坐標原點為框選容器的左上角頂點作為坐標原點即可。
換言之,就是修改mousedown跟mousemove事件時,初始位置由原來的e.clientX跟e.clientY修改為e.clientX - selectContaienr.offsetLeft + selectContainer.scrollLeft跟e.clientY - selectContainer.offsetTop + selectContainer.scrollTop。
坐標更改shi'yi'tu
<html> <head> <title>region</title> <style> body { margin: 0; padding: 0; } #selectContainer { position: relative; width: 400px; /* 演示寬高與位置 */ height: 400px; top: 200px; left: 200px; border: 1px solid #eee; overflow: hidden; overflow-y: auto; } .fileDiv { display: inline-block; width: 100px; height: 100px; margin: 24px; background-color: #0082CC; } </style> </head> <body> <div id="selectContainer"> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> <div class="fileDiv"></div> </div> </body> </html>
<script> (function () { var mouseStopId; var mouseOn = false; var startX = 0; var startY = 0; document.onmousedown = function (e) { clearEventBubble(e); if (e.buttons !== 1 || e.which !== 1) return; mouseStopId = setTimeout(function () { mouseOn = true; // 獲取容器元素 var selectContainer = document.getElementById('selectContainer'); // 調(diào)整坐標原點為容器左上角 startX = e.clientX - selectContainer.offsetLeft + selectContainer.scrollLeft; startY = e.clientY - selectContainer.offsetTop + selectContainer.scrollTop; var selDiv = document.createElement('div'); selDiv.style.cssText = 'position:absolute;width:0;height:0;margin:0;padding:0;border:1px dashed #eee;background-color:#aaa;z-index:1000;opacity:0.6;display:none;'; selDiv.id = 'selectDiv'; // 添加框選元素到容器內(nèi) document.getElementById('selectContainer').appendChild(selDiv); selDiv.style.left = startX + 'px'; selDiv.style.top = startY + 'px'; }, 300); } document.onmousemove = function (e) { if (!mouseOn) return; clearEventBubble(e); var selectContainer = document.getElementById('selectContainer'); var _x = e.clientX - selectContainer.offsetLeft + selectContainer.scrollLeft; var _y = e.clientY - selectContainer.offsetTop + selectContainer.scrollTop; var _H = selectContainer.clientHeight; // 鼠標移動超出容器內(nèi)部,進行相應的處理 // 向下拖拽 if (_y >= _H && selectContainer.scrollTop <= _H) { selectContainer.scrollTop += _y - _H; } // 向上拖拽 if (e.clientY <= selectContainer.offsetTop && selectContainer.scrollTop > 0) { selectContainer.scrollTop = Math.abs(e.clientY - selectContainer.offsetTop); } var selDiv = document.getElementById('selectDiv'); selDiv.style.display = 'block'; selDiv.style.left = Math.min(_x, startX) + 'px'; selDiv.style.top = Math.min(_y, startY) + 'px'; selDiv.style.width = Math.abs(_x - startX) + 'px'; selDiv.style.height = Math.abs(_y - startY) + 'px'; }; document.onmouseup = function (e) { if (!mouseOn) return; clearEventBubble(e); var selDiv = document.getElementById('selectDiv'); var fileDivs = document.getElementsByClassName('fileDiv'); var selectedEls = []; var l = selDiv.offsetLeft; var t = selDiv.offsetTop; var w = selDiv.offsetWidth; var h = selDiv.offsetHeight; for (var i = 0; i < fileDivs.length; i++) { var sl = fileDivs[i].offsetWidth + fileDivs[i].offsetLeft; var st = fileDivs[i].offsetHeight + fileDivs[i].offsetTop; if (sl > l && st > t && fileDivs[i].offsetLeft < l + w && fileDivs[i].offsetTop < t + h) { selectedEls.push(fileDivs[i]); } } console.log(selectedEls); selDiv.style.display = 'none'; mouseOn = false; }; function clearEventBubble (e) { if (e.stopPropagation) e.stopPropagation(); else e.cancelBubble = true; if (e.preventDefault) e.preventDefault(); else e.returnValue = false; } })(); </script>
使用前端框架
上邊的代碼,我們只是在一個html文件里邊實現(xiàn)了框選的功能。很多時候,我們會使用一些前端框架來編寫框選的功能(例如vue.js,angular,react,polymer之類的前端框架)。這個時候,我們可以利用框架自身的生命周期的函數(shù),添加對應的監(jiān)聽事件,然后在mouseup事件里移除掉上邊這些事件監(jiān)聽,以減少不必要的資源消耗。而且,很多時候,組件化的使用,使得被框選的元素,往往也是一個可重復利用的小組件,也是需要根據(jù)相應的框架的對應的途徑獲取到對應的DOM元素來獲取其屬性。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
微信小程序 wx:for 與 wx:for-items 與 wx:key的正確用法
這篇文章主要介紹了微信小程序 wx:for 與 wx:for-items 與 wx:key的正確用法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-05-05Javascript實現(xiàn)的鼠標經(jīng)過時播放聲音
今天突然想起做一個當鼠標經(jīng)過<a/>時,會發(fā)出聲音2010-05-05實現(xiàn)checkbox全選、反選、取消JavaScript小腳本異常
實現(xiàn)checkbox全選,反選,取消JavaScript小腳本的時出現(xiàn)點小異常,大家可以看看,以后就不要犯這個特別2的錯誤了2014-04-04