Kéo thả đối tượng bằng chuột
Trước khi bắt đầu, chúng ta hãy cùng nhau tạo một số hàm sẽ dùng tới nhiều lần trong loạt bài viết này. Về sau chúng ta xem đây như một thư viện đã được liên kết với các ứng dụng web mà không cần giải thích lại.
// Hàm `rel`, lấy ID của một đối tượng:
function rel(ob) {
return document.getElementById(ob);
}
// Hàm `getCursorPos`, trả về toạ độ hiện thời của con trỏ:
var cursor = {
x: 0,
y: 0
};
function getCursorPos(e) {
e = e || window.event;
if (e.pageX || e.pageY) {
cursor.x = e.pageX;
cursor.y = e.pageY;
} else {
var de = document.documentElement;
var db = document.body;
cursor.x = e.clientX +
(de.scrollLeft || db.scrollLeft) - (de.clientLeft || 0);
cursor.y = e.clientY +
(de.scrollTop || db.scrollTop) - (de.clientTop || 0);
}
return cursor;
}
getCursorPos
nhận vào một đối số e
, sau đó, tuỳ theo trình duyệt khách, script tiếp theo sẽ giúp chúng ta tìm được 2 giá trị cursor.x
và cursor.y
. Giá trị cursor.x
là khoảng cách từ con trỏ đến lề trái của trang. Còn cursor.y
là khoảng cách từ con trỏ đến đỉnh trên của trang.
Bây giờ, chúng ta cùng quan sát hình ảnh sau:
Ở đây, đối tượng mà chúng ta muốn di chuyển là hình chữ nhật ABCD. Hình này nằm ở vị trí cách đỉnh trang một khoảng AX, và cách lề trái một khoảng AY.
Bắt đầu của sự di chuyển, chúng ta đưa chuột vào trong hình chữ nhật và nhấp xuống tại điểm P. Với hàm getCursorPos
, chúng ta dễ dàng lấy được toạ độ của P. Chẳng hạn P cách đỉnh một khoảng pTop1
, cách biên trái 1 khoảng pLeft1
. Chúng ta cũng có thể xác định được khoảng cách từ P đến mỗi cạnh AB và AD :
h1 = pTop1 - AX ;
i1 = pLeft1 - AY ;
Vẫn giữ chuột như vậy, chúng ta rê đến điểm P'. Cũng bằng hàm getCursorPos
, chúng ta xác định vị trí tương đối của P' so với đỉnh và lề phải của trang là pTop2
và pLeft2
.
Khung chữ nhật ABCD sẽ di chuyển theo đến vị trí A'B'C'D'. Giả sử bạn nhả chuột ra tại P', việc mà chúng ta cần làm chỉ là thiết lập thuộc tính top
và left
cho hình chữ nhật ban đầu sao cho top = A'X'
và left=A'Y'
.
Nhìn vào hình minh hoạ, dễ nhận thấy:
A'X' = pTop2 - h2
A'Y' = pLeft2 - i2
Mặt khác, do sự di chuyển tịnh tiến, nên i2 = i1
và h2 = h1
.
Từ thực tế trên, chúng ta có ngay giải thuật để tạo ra một ứng dụng web với các đối tượng có thể kéo và thả như trên desktop.
Toàn bộ script chỉ đơn giản như dưới đây:
var dragobj = null,
h1, i1, oLeft, oTop;
function makeObjectToDrag(obj) {
dragobj = rel(obj.id);
document.onmousedown = startMove;
document.onmouseup = drop;
document.onmousemove = moving;
}
function startMove(e) {
getCursorPos(e);
dragobj.className = "moving";
i1 = cursor.x - dragobj.offsetLeft;
h1 = cursor.y - dragobj.offsetTop;
}
function drop() {
if (dragobj) {
dragobj.className = "move";
dragobj = null;
}
}
function moving(e) {
getCursorPos(e);
if (dragobj) {
oLeft = cursor.x - i1;
oTop = cursor.y - h1;
dragobj.style.left = oLeft + 'px';
dragobj.style.top = oTop + 'px';
}
}
Để đưa vào trang 1 đối tượng obj1 cho phép kéo - thả, chúng ta có thể viết trong hồ sơ HTML:
<div
id="obj1"
class="move"
onmousedown="makeObjectToDrag(this);">
</div>
Khi sự kiện mousedown xảy ra trên obj1, hàm makeObjectToDrag được gọi với tham số truyền vào là bản thân đối tượng.
Dòng code dragobj = rel(obj.id);
sẽ lấy ID của đối tượng và gán vào biến dragobj
. Tiếp theo mới đến phần
chỉ định các lệnh cần thực hiện tuỳ theo thao tác của người dùng:
document.onmousedown = startMove; //nhắp chuột xuống
document.onmouseup = drop; // thả chuột
document.onmousemove = moving; // di chuyển chuột
Nếu gọi hàm startMove
, chúng ta lấy toạ độ con trỏ (P) và thay đổi kiểu dáng cho đối tượng để phân biệt với trạng thái bình thường.
Sau đó chúng ta tính ra các khoảng i1 và h1 theo đúng lập luận đã nêu.
Nếu chuột vẫn được kéo đi mà chưa nhả, hàm moving thực thi. Hàm này cũng bắt đầu với việc xác định vị trí con trỏ tại các điểm mà nó di chuyển qua, nghĩa là 1 tập hợp của P'. Ở mỗi điểm P' này, chúng ta thiết lập lại các giá trị định vị của đối tượng đang được kéo đi:
dragobj.style.left = oLeft + 'px';
dragobj.style.top = oTop + 'px';
Khi sự kiện mouseup xảy ra, drop được gọi. Hàm này trả đối tượng về trang thái định dang ban đầu, ở vị trí mà nó được di chuyển tới.
Các bạn xem demo ở đây: http://codepen.io/ndaidong/full/PpBwVz/
Nâng cao thêm một chút
Một trang web được xây dựng như ví dụ trước cho phép chúng ta kéo thả các đối tượng trên nhiều nền trình duyệt khác nhau. Tôi đã test trên IE 6+, Opera 8+, Netscape 7+, FF, Flock. Không có vấn đề gì cả.
Tuy nhiên, xem cách mà chúng ta di chuyển đối tượng như vậy không giống với một ứng dụng desktop lắm. Trong Windows, khi một cửa sổ được di chuyển khỏi vị trí, chúng ta chỉ thấy một frame chuyển động theo trỏ chuột, cửa sổ chỉ chính thức được đặt vào vị trí mới khi mà thao tác drop xảy ra.
Với một chút nỗ lực, bạn cũng có thể làm cho ứng dụng web của chúng ta cư xử theo đúng cách này.
Có nhiều hơn một phương pháp. Ở đây tôi chọn phương pháp dễ hiểu nhất: khi nhấp vào đối tượng để kéo nó đi, tôi tạo ra một đối tượng mới có dạng khung bao quanh đối tượng chính.
Như một cái bóng, sẽ chỉ cần di chuyển cái bóng này. Với sự kiện mouseup, tôi làm ẩn khung ấy đi và thay đối tượng chính vào vị trí mà nó cần phải được đặt xuống.
Chúng ta sẽ viết thêm một hàm Shadow:
function Shadow(sLeft, sTop, sWidth, sHeight) {
elm = document.createElement('div');
document.body.appendChild(elm);
elm.className = "moving";
elm.style.left = sLeft + 'px';
elm.style.top = sTop + 'px';
elm.style.width = sWidth + 'px';
elm.style.height = sHeight + 'px';
this.onmousedown = makeObjectToDrag(this.name);
}
Hàm này định nghĩa một lớp đối tượng với các tham số chỉ định kích thước và vị trí tương đối xác lập sẵn mỗi khi khởi tạo. Các hiện thể của lớp này sẽ tồn tại trên trang web dưới dạng các thẻ DIV.
elm = document.createElement('div');
document.body.appendChild(elm);
Một sự kiện onmousedown xảy ra trên bất kỳ hiện thể nào của Shadow cũng chỉ định nó trở thành một đối tượng có thể kéo thả:
this.onmousedown = makeObjectToDrag(this.name);
Ban đầu tôi viết là makeObjectToDrag(this.id)
, nhưng trên IE xuất hiện thông báo lỗi mặc cho việc rê thả diễn ra bình thường. Thay bằng makeObjectToDrag(this.name)
thì không nó không phàn nàn gì nữa!
startMove
, moving
và drop
cũng được sửa lại một chút:
function startMove(e) {
getCursorPos(e);
i1 = cursor.x - dragobj.offsetLeft;
h1 = cursor.y - dragobj.offsetTop;
shadow = new Shadow(dragobj.offsetLeft, dragobj.offsetTop, 250, 150);
}
function moving(e) {
getCursorPos(e);
if (dragobj & amp; & amp; elm) {
oLeft = cursor.x - i1;
oTop = cursor.y - h1;
elm.style.left = oLeft + 'px';
elm.style.top = oTop + 'px';
}
}
function drop() {
if (dragobj) {
elm.className = 'hide';
elm = null;
dragobj.style.left = oLeft + 'px';
dragobj.style.top = oTop + 'px';
dragobj = null;
}
}
Như vậy, ở vào thời điểm bắt đầu thao tác drag, chúng ta khởi tạo 1 thể hiện của đối tượng Shadow:
shadow = new Shadow(dragobj.offsetLeft, dragobj.offsetTop, 250, 150);
Trong moving
, hàm kiểm soát quá trình drag, chúng ta không hề đả động tới dragobj
mà chỉ cập nhật vị trí cho elm
.
Với drop
, chúng ta làm cho Shadow ẩn đi bằng cách thay đổi lớp CSS của nó. Và giải phóng biến elm
. Cuối cùng, chúng ta đặt dragobj
vào đúng vị trí cần thiết.
Bây giờ, sự di chuyển đối tượng trên trang đã có vẻ pro hơn rồi!
Di chuyển đối tượng bằng bàn phím
Di chuyển một đối tượng bằng cách rê thả chuột cũng khá thú vị. Nhưng còn với bàn phím thì sao?
Chúng ta cũng có thể dùng các phím "mũi tên", hay bất kỳ phím nào đó, để điều khiển một đối tượng di chuyển trên trang web. Và dưới đây sẽ là một ví dụ.
Trên trang web này, tôi có 3 đối tượng. Khi click chuột vào 1 trong 3 đối tượng, trong hình là W, nó sẽ chuyển sang trạng thái focus, và bạn có thể dùng các phím mũi tên để di chuyển lên, xuống, sang phải, hoặc sang trái.
Phần HTML của 3 đối tượng như sau :
<div id="obj1" onclick="setFocus(this.id);">
Q
</div>
<div id="obj2" onclick="setFocus(this.id);">
W
</div>
<div id="obj3" onclick="setFocus(this.id);">
E
</div>
Và đây là các hàm JavaScript điều khiển:
var dragobj = null,
focusObj = null,
oLeft = 0,
oTop = 0,
speed = 7;
function setFocus(obj) {
setBlur();
if (obj) {
dragobj = rel(obj);
dragobj.style.backgroundColor = '#ff6666';
dragobj.style.borderStyle = 'dotted';
dragobj.style.zIndex = 3;
document.onkeydown = startMove;
document.onkeypress = moving;
document.onkeydown = moving;
focusObj = dragobj;
}
}
function startMove() {
if (dragobj) {
oLeft = dragobj.offsetLeft;
oTop = dragobj.offsetTop;
}
}
function setBlur() {
if (focusObj) {
focusObj.style.backgroundColor = '#ff00ff';
focusObj.style.borderStyle = 'double';
focusObj.style.zIndex = 1;
focusObj = null;
}
}
function moving(e) {
if (dragobj) {
updatePos(e);
dragobj.style.left = oLeft + 'px';
dragobj.style.top = oTop + 'px';
}
}
function updatePos(Ev) {
oLeft = dragobj.offsetLeft;
oTop = dragobj.offsetTop;
if (!Ev) var Ev = window.event;
var iKeyCode = Ev.keyCode;
if (Ev.keyCode) iKeyCode = Ev.keyCode;
else if (Ev.which) iKeyCode = Ev.which;
switch (iKeyCode) {
case 37:
oLeft -= speed;
break;
case 38:
oTop -= speed;
break;
case 39:
oLeft += speed;
break;
case 40:
oTop += speed;
break;
default:
break;
}
}
Khi chúng ta click lên 1 đối tượng, hàm setFocus
được gọi cùng với tham số là ID của đối tượng đó.
setFocus
bắt đầu bằng việc gỡ bỏ sự tập trung vào đối tượng đã được điều khiển trước đó. Chẳng hạn chúng ta đã nhấp vào Q, rồi nhấp sang E, trạng thái của Q phải được phục hồi. Điều này thực hiện bởi setBlur
:
function setBlur() {
if (focusObj) {
focusObj.style.backgroundColor = '#ff00ff';
focusObj.style.borderStyle = 'double';
focusObj.style.zIndex = 1;
focusObj = null;
}
}
setBlur
kiểm tra biến focusObj
có tồn tại hay không, nghĩa là trước đó, bạn đã điều khiển đối tượng nào khác không. Nếu có, chúng ta đặt lại một số thuộc tính định dạng cho giống với trạng thái khởi đầu. Và trả focusObj
về null.
Trở lại với setFocus
, chúng ta chuyển đối tượng sang trạng thái được kích hoạt cũng bằng cách thay đổi vài thiết lập CSS.
Các dòng:
document.onkeydown = startMove;
document.onkeypress = moving;
document.onkeydown = moving;
sẽ định nghĩa những xử lý khi ghi nhận được những tác động từ bàn phím. document.onkeydown=moving
sẽ hữu dụng trong IE, còn với Mozilla thì không cần dòng này.
Cuối cùng, ta lưu đối tượng vào biến focusObj
để sử dụng cho những lần kích hoạt sau.
Khi 1 phím được nhấn trên đối tượng, sự kiện onkeydown gọi hàm startMove
đánh dấu khởi điểm của quá trình di chuyển đối tượng. Nó chỉ có việc xác định vị trí hiện thời của đối tượng và đưa vào 2 biến oLeft
, oTop
.
Nếu phím vẫn được giữ (onkeypress), chúng ta gọi hàm moving. Hàm này tính toán các giá trị oLeft
, oTop
rồi cập nhật vào các thuộc tính top
, letf
của đối tượng. Và trên màn hình chúng ta thấy đối tượng dịch chuyển.
Việc tính toán các giá trị oLeft
, oTop
được đảm trách bởi hàm updatePos
. Hàm này nhận một đối số Ev
, trong IE, Ev
là một đối tượng nằm dưới lớp window - window.event, còn Netscape tự động tạo ra tham số Ev tồn tại độc lập như navigator, document...
Với IE, Ev.keyCode
trả về mã của phím được nhấn. Với Netscape, mã phím nằm trong Ev.which
. Đây là những con số, và trong trường hợp này chúng ta chỉ quan tâm đến 4 giá trị:
- 37: Left Arrow
- 38: Up Arrow
- 39: Right Arrow
- 40: Down Arrow
Khi phím mũi tên sang trái (Left Arrow) được nhấn, chúng ta cho đối tượng dịch chuyển về bên trái màn hình bằng cách giảm giá trị oLeft đi một khoảng bằng speed. Đây là biến được khởi tạo ban đầu, speed càng lớn thì sự di chuyển diễn ra càng nhanh.
case 37 : oLeft-=speed ; break;
Khi phím mũi tên sang phải (Right Arrow) được nhấn, chúng ta cho đối tượng dịch chuyển về bên phải màn hình bằng cách tăng giá trị oLeft lên.
Tương tự như vậy, chúng ta tăng hoặc giảm oTop.
Và cuối cùng, chúng ta có thể thu được kết quả như trang demo dưới đây:
* Lưu ý:
Trong bản demo này, tôi viết thêm một hàm setObjByKeyCode
nữa
function setObjByKeyCode(e) {
if (!e) var e = window.event;
var code;
if (e.keyCode) code = e.keyCode;
else if (e.which) code = e.which;
var character = String.fromCharCode(code);
switch (character) {
case 'q':
setFocus('obj1');
break;
case 'w':
setFocus('obj2');
break;
case 'e':
setFocus('obj3');
break;
default:
break;
}
}
document.onkeypress = setObjByKeyCode;
Và sửa lại updatePos
như sau:
function updatePos(Ev) {
setObjByKeyCode(Ev);
oLeft = dragobj.offsetLeft;
oTop = dragobj.offsetTop;
if (!Ev) var Ev = window.event;
var iKeyCode = Ev.keyCode;
if (Ev.keyCode) iKeyCode = Ev.keyCode;
else if (Ev.which) iKeyCode = Ev.which;
switch (iKeyCode) {
case 37:
oLeft -= speed;
break;
case 38:
oTop -= speed;
break;
case 39:
oLeft += speed;
break;
case 40:
oTop += speed;
break;
default:
break;
}
}
Điều này cho phép người dùng có thể nhấn vào các phím Q, W, E để chọn đối tượng tương ứng. Về nguyên tác vẫn dựa trên mã phím nhập vào. Nhưng với phương thức String.fromCharCode
, chúng ta xác định được ký tự mà phím đó nắm giữ.