Việc tạo ra một hộp thoại (modal dialog) có khả năng kéo thả là một tính năng tương tác phổ biến trên các ứng dụng web. Bài viết này sẽ hướng dẫn bạn cách triển khai một hộp thoại đăng nhập cơ bản với khả năng kéo thả bằng cách sử dụng HTML, CSS và JavaScript thuần túy, tận dụng các API của DOM (Document Object Model).
1. Cấu Trúc HTML
Chúng ta sẽ xây dựng cấu trúc cho hộp thoại bao gồm một liên kết để mở hộp thoại, một lớp phủ nền (overlay) và chính hộp thoại đăng nhập. Hộp thoại sẽ có tiêu đề (có thể kéo thả), các trường nhập liệu và nút đóng/đăng nhập.
<!-- Liên kết để mở hộp thoại -->
<a href="#" id="openDialogLink" class="open-dialog-link">Mở Hộp Thoại Đăng Nhập</a>
<!-- Lớp phủ nền mờ -->
<div id="dialogOverlay" class="dialog-overlay"></div>
<!-- Hộp thoại kéo thả -->
<div id="draggableDialog" class="custom-dialog">
<!-- Phần tiêu đề của hộp thoại, là vùng để kéo thả -->
<div id="dialogHeader" class="dialog-header">
<span>Đăng Nhập Thành Viên</span>
<button id="closeDialogButton" class="dialog-close-button">×</button>
</div>
<!-- Phần nội dung của hộp thoại -->
<div class="dialog-body">
<div class="form-group">
<label for="usernameInput">Tài khoản:</label>
<input type="text" id="usernameInput" placeholder="Nhập tài khoản của bạn">
</div>
<div class="form-group">
<label for="passwordInput">Mật khẩu:</label>
<input type="password" id="passwordInput" placeholder="Nhập mật khẩu">
</div>
</div>
<!-- Phần chân hộp thoại -->
<div class="dialog-footer">
<button id="submitLoginButton" class="action-button">Đăng Nhập</button>
</div>
</div>
2. Tạo Phong Cách Với CSS
CSS sẽ định hình giao diện của hộp thoại và lớp phủ nền, đồng thời thiết lập các thuộc tính cần thiết cho tính năng kéo thả như position: absolute và cursor.
<style>
/* CSS cho lớp phủ nền */
.dialog-overlay {
position: fixed; /* Cố định trên màn hình */
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* Nền đen mờ */
display: none; /* Ban đầu ẩn */
z-index: 9000; /* Đảm bảo nằm trên các phần tử khác */
}
/* CSS cho hộp thoại tùy chỉnh */
.custom-dialog {
position: absolute; /* Quan trọng cho kéo thả */
width: 500px;
background-color: #fff;
border: 1px solid #ddd;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
z-index: 9001; /* Nằm trên lớp phủ */
display: none; /* Ban đầu ẩn */
/* Canh giữa hộp thoại ban đầu bằng transform */
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
/* Phần tiêu đề của hộp thoại, cũng là vùng kéo thả */
.dialog-header {
padding: 15px;
background-color: #f7f7f7;
border-bottom: 1px solid #eee;
cursor: grab; /* Thay đổi con trỏ chuột để báo hiệu có thể kéo */
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
user-select: none; /* Ngăn chặn việc chọn văn bản khi kéo */
}
.dialog-header:active {
cursor: grabbing; /* Con trỏ chuột khi đang kéo */
}
/* Nút đóng hộp thoại */
.dialog-close-button {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #888;
padding: 0 5px;
line-height: 1;
outline: none;
}
.dialog-close-button:hover {
color: #555;
}
/* Nội dung và chân hộp thoại */
.dialog-body {
padding: 20px;
}
.form-group {
margin-bottom: 15px;
display: flex;
align-items: center;
}
.form-group label {
width: 100px;
flex-shrink: 0;
text-align: right;
padding-right: 15px;
font-size: 14px;
}
.form-group input {
flex-grow: 1;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.dialog-footer {
padding: 15px;
border-top: 1px solid #eee;
text-align: center;
}
.action-button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
outline: none;
}
.action-button:hover {
background-color: #0056b3;
}
/* CSS cho liên kết mở hộp thoại */
.open-dialog-link {
display: block;
margin: 20px;
text-align: right;
font-size: 18px;
color: #007bff;
text-decoration: none;
}
.open-dialog-link:hover {
text-decoration: underline;
}
</style>
3. Logic Kéo Thả và Hiển Thị Hộp Thoại với JavaScript
Phần JavaScript sẽ quản lý việc hiển thị/ẩn hộp thoại và xử lý sự kiện kéo thả. Chúng ta sẽ sử dụng addEventListener để gắn các trình xử lý sự kiện một cách hiện đại.
<script>
document.addEventListener('DOMContentLoaded', () => {
// Lấy các phần tử DOM cần thiết
const openLink = document.getElementById('openDialogLink');
const dialogBox = document.getElementById('draggableDialog');
const overlay = document.getElementById('dialogOverlay');
const closeButton = document.getElementById('closeDialogButton');
const dialogTitleBar = document.getElementById('dialogHeader');
// Hàm hiển thị hộp thoại và lớp phủ
function showDialog() {
dialogBox.style.display = 'block';
overlay.style.display = 'block';
}
// Hàm ẩn hộp thoại và lớp phủ
function hideDialog() {
dialogBox.style.display = 'none';
overlay.style.display = 'none';
}
// Gắn sự kiện click để mở hộp thoại
openLink.addEventListener('click', (event) => {
event.preventDefault(); // Ngăn chặn hành vi mặc định của thẻ 'a'
showDialog();
});
// Gắn sự kiện click để đóng hộp thoại
closeButton.addEventListener('click', hideDialog);
// --- Logic kéo thả hộp thoại ---
let isDragging = false; // Biến trạng thái để kiểm tra xem có đang kéo hay không
let initialMouseX, initialMouseY; // Vị trí chuột ban đầu khi bắt đầu kéo
let initialDialogX, initialDialogY; // Vị trí hộp thoại ban đầu
dialogTitleBar.addEventListener('mousedown', (event) => {
event.preventDefault(); // Ngăn chặn hành vi chọn văn bản mặc định của trình duyệt
isDragging = true;
// Lưu trữ vị trí chuột khi sự kiện 'mousedown' xảy ra
initialMouseX = event.clientX;
initialMouseY = event.clientY;
// Lấy vị trí hiện tại của hộp thoại (tính toán sau khi áp dụng transform)
const dialogRect = dialogBox.getBoundingClientRect();
initialDialogX = dialogRect.left;
initialDialogY = dialogRect.top;
// Xóa thuộc tính 'transform' để tránh xung đột với việc cập nhật 'left'/'top' trực tiếp
dialogBox.style.transform = 'none';
// Gắn các trình xử lý sự kiện 'mousemove' và 'mouseup' vào 'document'
// để đảm bảo hộp thoại vẫn được kéo ngay cả khi chuột di chuyển ra khỏi vùng tiêu đề
document.addEventListener('mousemove', onDrag);
document.addEventListener('mouseup', onStopDrag);
});
// Hàm xử lý sự kiện 'mousemove' khi đang kéo
function onDrag(event) {
if (!isDragging) return; // Chỉ xử lý nếu đang trong trạng thái kéo
// Tính toán sự thay đổi vị trí chuột
const deltaX = event.clientX - initialMouseX;
const deltaY = event.clientY - initialMouseY;
// Cập nhật vị trí mới của hộp thoại
dialogBox.style.left = (initialDialogX + deltaX) + 'px';
dialogBox.style.top = (initialDialogY + deltaY) + 'px';
}
// Hàm xử lý sự kiện 'mouseup' khi thả chuột
function onStopDrag() {
isDragging = false; // Chuyển trạng thái về không kéo
// Gỡ bỏ các trình xử lý sự kiện để tránh rò rỉ bộ nhớ và hành vi không mong muốn
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('mouseup', onStopDrag);
}
// Tùy chọn: Đóng hộp thoại khi click vào vùng phủ nền
overlay.addEventListener('click', hideDialog);
});
</script>
Trong phần JavaScript, khi người dùng nhấn chuột xuống (mousedown) trên thanh tiêu đề của hộp thoại, chúng ta lưu lại vị trí chuột và vị trí hiện tại của hộp thoại. Khi chuột di chuyển (mousemove), hộp thoại sẽ được dịch chuyển theo sự thay đổi của con trỏ chuột. Cuối cùng, khi chuột được thả ra (mouseup), các trình xử lý sự kiện kéo sẽ được gỡ bỏ để dừng quá trình kéo.
Lưu ý rằng chúng ta sử dụng getBoundingClientRect() để lấy vị trí ban đầu của hộp thoại một cách chính xác sau khi transform: translate(-50%, -50%) đã được áp dụng, và sau đó thiết lập dialogBox.style.transform = 'none' để chuyển sang điều khiển vị trí bằng left và top trực tiếp. Điều này đảm bảo tính nhất quán trong quá trình kéo.