使用原生js編寫(xiě)一個(gè)簡(jiǎn)單的框選功能方法
今天我們來(lái)聊一下怎么使用原生javascript編寫(xiě)一個(gè)簡(jiǎn)單的框選功能。
需求描述
- 鼠標(biāo)左鍵按下不放,移動(dòng)鼠標(biāo)出現(xiàn)矩形選框;
- 鼠標(biāo)左鍵松開(kāi),根據(jù)上邊出現(xiàn)的矩形選框統(tǒng)計(jì)選框范圍內(nèi)的DOM元素;
嗯...上邊的功能描述看著是挺簡(jiǎn)單的,但實(shí)現(xiàn)起來(lái)也還是會(huì)有些地方需要斟酌思考的。比如,如果我們的框選范圍不是document.body,而是某一個(gè)div里邊進(jìn)行框選呢?而現(xiàn)實(shí)開(kāi)發(fā)過(guò)程中,我們會(huì)遇上的應(yīng)該就是第二種情況。
怎么實(shí)現(xiàn)
二話不說(shuō),咱們動(dòng)手寫(xiě)代碼吧!因?yàn)楦玫募嫒菪裕@里就避免了一些ES6的語(yǔ)法,如果是用的其他框架來(lái)寫(xiě)的話,代碼上相應(yīng)的也要做一些調(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>
添加鼠標(biāo)事件監(jiān)聽(tīng)
由于js自身并沒(méi)有帶有鼠標(biāo)點(diǎn)擊按住不放這樣子的事件,這里我們不僅需要檢測(cè)鼠標(biāo)左鍵點(diǎn)擊按下,還要加一個(gè)定時(shí)器來(lái)檢測(cè)鼠標(biāo)是否按住不放了。
<script>
(function () {
// 定時(shí)器id
var mouseStopId;
// 是否開(kāi)啟框選功能
var mouseOn = false;
// 用來(lái)存放鼠標(biāo)點(diǎn)擊初始位置
var startX = 0;
var startY = 0;
// 添加鼠標(biāo)按下監(jiān)聽(tīng)事件
document.body.addEventListener('mousedown', function (e) {
// 阻止事件冒泡
clearEventBubble(e);
// 判斷是否為鼠標(biāo)左鍵被按下
if (e.buttons !== 1 || e.which !== 1) return;
mouseStopId = setTimeout(function () {
mouseOn = true;
startX = e.clientX;
startY = e.clientY;
}, 300); // 間隔300毫秒后執(zhí)行,判定這時(shí)候鼠標(biāo)左鍵被按住不放
});
// 添加鼠標(biāo)移動(dòng)事件監(jiān)聽(tīng)
document.body.addEventListener('mousemove', function (e) {
// 如果并非框選開(kāi)啟,退出
if (!mouseOn) return;
// 阻止事件冒泡
clearEventBubble(e);
// 處理鼠標(biāo)移動(dòng)
// codes
});
// 添加鼠標(biāo)點(diǎn)擊松開(kāi)事件監(jiān)聽(tīng)
document.body.addEventListener('mouseup', function (e) {
// 阻止事件冒泡
clearEventBubble(e);
// 處理鼠標(biāo)點(diǎn)擊松開(kāi)
// 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)聽(tīng)還不夠,為了更好的交互效果,我們需要一個(gè)隨時(shí)跟隨著鼠標(biāo)移動(dòng)的框選框元素,用于讓用戶隨時(shí)感知框選范圍。
<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)建一個(gè)框選元素
var selDiv = document.createElement('div');
// 給框選元素添加css樣式,這里使用絕對(duì)定位
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);
// 獲取當(dāng)前坐標(biāo)
var _x = e.clientX;
var _y = e.clientY;
// 根據(jù)坐標(biāo)給選框修改樣式
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';
// 如果需要更直觀一點(diǎn)的話,我們還可以在這里進(jìn)行對(duì)框選元素覆蓋到的元素進(jìn)行修改被框選樣式的修改。
});
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>
添加鼠標(biāo)松開(kāi)事件監(jiān)聽(tīng)

元素是否被選中示意圖
我們沒(méi)有在鼠標(biāo)移動(dòng)的時(shí)候去實(shí)時(shí)統(tǒng)計(jì)被框選到的DOM元素,如果需要實(shí)時(shí)統(tǒng)計(jì)或者實(shí)時(shí)修改被選擇的DOM元素的樣式,以便更準(zhǔn)確的讓用戶感知到被框選的內(nèi)容的話,可以選擇在mousemove事件里邊去實(shí)現(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';
};
// 添加鼠標(biāo)松開(kāi)事件監(jiān)聽(tīng)
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元素被選中,進(jìn)行處理
selectedEls.push(fileDivs[i]);
}
}
// 打印被選中DOM元素
console.log(selectedEls);
// 恢復(fù)參數(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>
這里判斷一個(gè)元素是否被選中采用的判斷條件是:
- 該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ù)值要?。?/li>
- 該DOM元素的最上邊(fileDiv[i].offsetTop)是否要比選框元素的最下邊(selDiv.offsetTop + selDiv.offsetHeight)的位置數(shù)值要?。?/li>
滿足了以上四個(gè)條件,即可判別為該DOM元素被選中了。
實(shí)際應(yīng)用
上邊的例子,舉得有些過(guò)于簡(jiǎn)單了。實(shí)際的開(kāi)發(fā)當(dāng)中,框選的范圍往往不可能是整個(gè)document.body,而是某一個(gè)具體的有特定寬度跟高度限制的元素。這個(gè)時(shí)候,就還需要考慮這個(gè)框選容器元素造成的定位偏差,以及容器內(nèi)元素過(guò)多,出現(xiàn)滾動(dòng)條的情況了。
乍一看,上邊的情況需要考慮的因素多了不少,比較容易亂。我這里采用的方法是修改坐標(biāo)系的方式來(lái)實(shí)現(xiàn)上邊描述的功能。上文我們已經(jīng)實(shí)現(xiàn)了在document.body整個(gè)頁(yè)面左上角頂點(diǎn)作為坐標(biāo)原點(diǎn)來(lái)實(shí)現(xiàn)框選功能,這時(shí)候我們需要修改坐標(biāo)原點(diǎn)為框選容器的左上角頂點(diǎn)作為坐標(biāo)原點(diǎn)即可。
換言之,就是修改mousedown跟mousemove事件時(shí),初始位置由原來(lái)的e.clientX跟e.clientY修改為e.clientX - selectContaienr.offsetLeft + selectContainer.scrollLeft跟e.clientY - selectContainer.offsetTop + selectContainer.scrollTop。

坐標(biāo)更改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)整坐標(biāo)原點(diǎn)為容器左上角
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;
// 鼠標(biāo)移動(dòng)超出容器內(nèi)部,進(jìn)行相應(yīng)的處理
// 向下拖拽
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>
使用前端框架
上邊的代碼,我們只是在一個(gè)html文件里邊實(shí)現(xiàn)了框選的功能。很多時(shí)候,我們會(huì)使用一些前端框架來(lái)編寫(xiě)框選的功能(例如vue.js,angular,react,polymer之類的前端框架)。這個(gè)時(shí)候,我們可以利用框架自身的生命周期的函數(shù),添加對(duì)應(yīng)的監(jiān)聽(tīng)事件,然后在mouseup事件里移除掉上邊這些事件監(jiān)聽(tīng),以減少不必要的資源消耗。而且,很多時(shí)候,組件化的使用,使得被框選的元素,往往也是一個(gè)可重復(fù)利用的小組件,也是需要根據(jù)相應(yīng)的框架的對(duì)應(yīng)的途徑獲取到對(duì)應(yīng)的DOM元素來(lái)獲取其屬性。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
IScroll5 中文API參數(shù)說(shuō)明和調(diào)用方法
IScroll是移動(dòng)頁(yè)面上被使用的一款仿系統(tǒng)滾動(dòng)插件。IScroll5相對(duì)于之前的IScroll4改進(jìn)了許多,使得大家可以更方便的定制所需的功能了。2016-05-05
JS實(shí)現(xiàn)自適應(yīng)高度表單文本框的方法
這篇文章主要介紹了JS實(shí)現(xiàn)自適應(yīng)高度表單文本框的方法,實(shí)例分析了針對(duì)IE內(nèi)核與非IE內(nèi)核下的javascript控制文本框樣式的技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-02-02
十個(gè)優(yōu)秀的Ajax/Javascript實(shí)例網(wǎng)站收集
今天,要向大家推薦10個(gè)相當(dāng)棒的Ajax和Javascript國(guó)外資源網(wǎng)站或博客,它們提供了相當(dāng)多的高質(zhì)量Ajax、Javascript實(shí)例及教程,喜歡Ajax和Javascript的朋友絕對(duì)不能錯(cuò)過(guò)。2010-03-03
用js代碼改變單選框選中狀態(tài)的簡(jiǎn)單實(shí)例
這篇文章主要介紹了js代碼改變單選框選中狀態(tài)的簡(jiǎn)單實(shí)例,有需要的朋友可以參考一下2013-12-12
在js中判斷checkboxlist(.net控件客戶端id)是否有選中
添加或修改內(nèi)容時(shí),需要對(duì)關(guān)鍵數(shù)據(jù)進(jìn)行判空處理,checkboxlist是否有選擇項(xiàng)如何使用js判斷實(shí)現(xiàn),接下來(lái)為大家詳細(xì)介紹下實(shí)現(xiàn)方法,感興趣的朋友可以參考下哈2013-04-04
JS模擬實(shí)現(xiàn)ECMAScript5新增的數(shù)組方法
ECMAScript5 新增了十個(gè)數(shù)組方法,這些方法只有在ie9及以上瀏覽器中可以被使用,下面是對(duì)于這些方法的模擬實(shí)現(xiàn)簡(jiǎn)單介紹下,需要的朋友參考下2017-03-03
在IE下:float屬性會(huì)影響offsetTop的取值
在IE下:float屬性會(huì)影響offsetTop的取值...2006-12-12
js去除重復(fù)字符串兩種實(shí)現(xiàn)方法
js去除重復(fù)字符串在項(xiàng)目開(kāi)發(fā)中很實(shí)用,接下來(lái)詳細(xì)介紹實(shí)現(xiàn)方法,感興趣的朋友可以參考下2013-01-01

