"Khám phá hướng dẫn chi tiết cách tích hợp modal chọn ảnh từ thư viện vào ứng dụng CodeIgniter của bạn, hoàn chỉnh với tính năng phân trang tiện lợi. Bài viết này sẽ hướng dẫn bạn từng bước từ cấu hình controller PHP đến xây dựng giao diện HTML, CSS và logic JavaScript (hướng đối tượng) để quản lý ảnh một cách hiệu quả. Đặc biệt, bạn sẽ học cách đồng bộ hóa trạng thái giữa việc tải lên ảnh mới và chọn ảnh có sẵn, đảm bảo trải nghiệm người dùng liền mạch và dữ liệu nhất quán. Đây là giải pháp lý tưởng cho việc nâng cao chức năng quản lý hình ảnh trong các biểu mẫu hoặc trình soạn thảo như CKEditor."
Hướng dẫn này sẽ giúp bạn triển khai một modal tùy chỉnh để chọn ảnh từ thư viện với chức năng phân trang, tích hợp tốt với các trường hợp sử dụng trong CKEditor hoặc các biểu mẫu khác, đồng thời quản lý nhất quán giữa việc tải lên file mới và chọn ảnh có sẵn.
---application/controllers/Upload.php
)Đây là nơi xử lý logic lấy danh sách ảnh từ máy chủ với phân trang.
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Upload extends CI_Controller {
public function __construct() {
parent::__construct();
$this->load->helper('url');
$this->load->helper('file');
}
public function index() {
// Đổi tên view để phân biệt
$this->load->view('upload_view_custom_modal_paginated');
}
// Phương thức để lấy ảnh với phân trang
public function get_images_paginated() {
$base_upload_dir = 'upload/'; // Đường dẫn gốc của thư mục upload
// Lấy tham số trang hiện tại và giới hạn số ảnh từ AJAX request
$page = $this->input->get('page') ? (int)$this->input->get('page') : 1;
$limit = $this->input->get('limit') ? (int)$this->input->get('limit') : 60; // Mặc định 60 ảnh/trang
$all_images = [];
$this->_scan_dir_for_images($base_upload_dir, $all_images);
// Sắp xếp ảnh theo tên để đảm bảo thứ tự nhất quán khi phân trang
sort($all_images);
$total_images = count($all_images);
$total_pages = ceil($total_images / $limit);
// Tính toán offset (vị trí bắt đầu của các ảnh cho trang hiện tại)
$offset = ($page - 1) * $limit;
// Lấy các ảnh cho trang hiện tại
$paged_images = array_slice($all_images, $offset, $limit);
// Trả về dữ liệu phân trang
echo json_encode([
'images' => $paged_images,
'total_images' => $total_images,
'total_pages' => $total_pages,
'current_page' => $page,
'limit' => $limit
]);
}
/**
* Hàm đệ quy để quét các thư mục và lấy tất cả ảnh.
* @param string $dir_path Đường dẫn thư mục hiện tại để quét.
* @param array $images Mảng tham chiếu để lưu trữ các URL ảnh tìm được.
*/
private function _scan_dir_for_images($dir_path, &$images) {
if (substr($dir_path, -1) !== '/') {
$dir_path .= '/';
}
$scan = @scandir($dir_path);
if ($scan === false) {
log_message('error', 'Không thể quét thư mục: ' . $dir_path);
return;
}
foreach ($scan as $item) {
if ($item == '.' || $item == '..') {
continue;
}
$item_path = $dir_path . $item;
if (is_dir($item_path)) {
$this->_scan_dir_for_images($item_path, $images);
} else if (is_file($item_path)) {
$file_extension = pathinfo($item_path, PATHINFO_EXTENSION);
$allowed_extensions = ['jpg', 'jpeg', 'png', 'gif'];
if (in_array(strtolower($file_extension), $allowed_extensions)) {
$images[] = base_url($item_path);
}
}
}
}
// Phương thức xử lý tải ảnh lên (giữ nguyên)
public function do_upload() {
$config['upload_path'] = './upload/';
$config['allowed_types'] = 'gif|jpg|png';
$config['max_size'] = 1024; // 1MB
$config['max_width'] = 1024;
$config['max_height'] = 768;
$this->load->library('upload', $config);
if ( ! $this->upload->do_upload('userfile')) {
$error = array('error' => $this->upload->display_errors());
echo json_encode($error);
} else {
$data = array('upload_data' => $this->upload->data());
echo json_encode(['success' => true, 'file_name' => $data['upload_data']['file_name']]);
}
}
}
---
Tạo file application/views/upload_view_custom_modal_paginated.php
và dán toàn bộ mã dưới đây vào. Mã JavaScript đã được tổ chức theo hướng đối tượng và xử lý logic đồng bộ giữa input file và input ẩn.
<!DOCTYPE html>
<html>
<head>
<title>Upload Hình ảnh (Custom Modal - Phân trang)</title>
<style>
/* CSS cho modal tùy chỉnh */
.custom-modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1000;
justify-content: center;
align-items: center;
}
.custom-modal-content {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
max-width: 900px; /* Điều chỉnh giá trị này để có chiều rộng mong muốn */
width: 90%; /* Hoặc dùng width: 90% */
min-width: 500px; /* Thêm min-width để tránh bị quá hẹp */
max-height: 90vh;
position: relative;
display: flex;
flex-direction: column;
}
.custom-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
margin-bottom: 15px;
flex-shrink: 0;
}
.custom-modal-title {
margin: 0;
font-size: 1.5em;
}
.custom-modal-close-btn {
background: none;
border: none;
font-size: 2em;
cursor: pointer;
color: #aaa;
line-height: 1;
position: absolute;
top: 10px;
right: 15px;
}
.custom-modal-close-btn:hover {
color: #666;
}
.custom-modal-body {
flex-grow: 1;
overflow-y: auto; /* Giữ overflow-y ở đây để cuộn nội dung nếu cần */
padding-right: 10px;
padding-bottom: 10px;
}
/* CSS cho lưới ảnh */
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); /* Điều chỉnh giá trị 120px này */
gap: 10px;
width: 100%;
}
.image-grid img {
width: 100%;
height: 100px; /* Chiều cao cố định cho tất cả ảnh */
object-fit: cover; /* Cắt ảnh để vừa với kích thước mà không làm biến dạng */
cursor: pointer;
border: 2px solid transparent;
display: block;
}
.image-grid img:hover {
border: 2px solid blue;
}
/* CSS cho phân trang */
.pagination-controls {
display: flex;
justify-content: center;
align-items: center;
margin-top: 15px;
flex-shrink: 0;
}
.pagination-controls button {
background-color: #f0f0f0;
border: 1px solid #ddd;
padding: 8px 12px;
margin: 0 5px;
cursor: pointer;
border-radius: 4px;
}
.pagination-controls button:hover:not(:disabled) {
background-color: #e0e0e0;
}
.pagination-controls button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.pagination-controls span {
font-weight: bold;
margin: 0 10px;
}
/* Các kiểu cơ bản cho form-group để dễ nhìn */
.form-group {
margin-bottom: 15px;
}
.col-sm-12 {
margin-bottom: 10px;
}
.btn {
padding: 8px 15px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="container">
<div class="form-group">
<label class="col-sm-12">Hình ảnh</label>
<div class="col-sm-12">
<input type="file" name="userfile" id="input_img" onchange="handleFiles()">
<button type="button" class="btn" id="openImageModalBtn">Chọn ảnh từ thư viện</button>
</div>
<div class="clearfix"></div>
<br>
<div class="col-sm-12" id="view_img">
<img src="https://vanhiep.net/img/noimage.png" id="noimage_review" style="max-width: 200px; height: auto; display: block;">
<!-- Input ẩn để lưu đường dẫn ảnh từ thư viện -->
<input type="hidden" name="link_image_thump" id="link_image_thump" value="">
</div>
</div>
</div>
<!-- Custom Modal -->
<div class="custom-modal-overlay" id="imageModal">
<div class="custom-modal-content">
<div class="custom-modal-header">
<h4 class="custom-modal-title">Chọn hình ảnh</h4>
<button type="button" class="custom-modal-close-btn" id="closeImageModalBtn">×</button>
</div>
<div class="custom-modal-body">
<div class="image-grid" id="imageGridContent">
<!-- Images will be loaded here via AJAX -->
</div>
</div>
<!-- Vùng điều khiển phân trang -->
<div class="pagination-controls" id="paginationControls">
<button id="prevPageBtn" disabled>← Trang trước</button>
<span id="currentPageInfo">Trang 1 / 1</span>
<button id="nextPageBtn" disabled>Trang sau →</button>
</div>
</div>
</div>
<!-- Sử dụng jQuery để đơn giản hóa AJAX và thao tác DOM -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script>
// Hàm xử lý file trực tiếp (khi người dùng chọn file để tải lên)
function handleFiles() {
const fileInput = document.getElementById('input_img');
const imgPreview = document.getElementById('noimage_review'); // Hoặc id bạn muốn dùng
if (fileInput.files && fileInput.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
imgPreview.src = e.target.result;
};
reader.readAsDataURL(fileInput.files[0]);
// Khi người dùng chọn file mới, xóa giá trị của input link_image_thump
$('#link_image_thump').val('');
}
}
// Định nghĩa lớp ImageSelector để quản lý modal chọn ảnh và phân trang
class ImageSelector {
constructor(options) {
// Khai báo các thuộc tính (elements và trạng thái)
this.modal = $(options.modalId);
this.imageGridContent = $(options.imageGridContentId);
this.prevPageBtn = $(options.prevPageBtnId);
this.nextPageBtn = $(options.nextPageBtnId);
this.currentPageInfo = $(options.currentPageInfoId);
this.openModalBtn = $(options.openModalBtnId);
this.closeModalBtn = $(options.closeModalBtnId);
this.targetImageReviewIds = options.targetImageReviewIds || []; // Mảng các ID để cập nhật ảnh xem trước
this.linkImageThumpInput = $(options.linkImageThumpInputId); // Input ẩn lưu URL ảnh từ thư viện
this.fileInput = $(options.fileInputId); // Input type="file"
this.currentPage = 1;
this.imagesPerPage = options.imagesPerPage || 60;
this.ajaxUrl = options.ajaxUrl;
// Gọi phương thức khởi tạo sự kiện
this.initEvents();
}
initEvents() {
// Sự kiện mở modal
this.openModalBtn.on('click', () => {
this.currentPage = 1; // Reset về trang đầu tiên mỗi khi mở modal
this.loadImagesIntoModal(this.currentPage);
this.modal.css('display', 'flex'); // Hiển thị modal
});
// Sự kiện đóng modal
this.closeModalBtn.on('click', () => {
this.modal.css('display', 'none'); // Ẩn modal
});
// Sự kiện click ra ngoài modal để đóng
this.modal.on('click', (event) => {
if ($(event.target).is(this.modal)) {
this.modal.css('display', 'none');
}
});
// Sự kiện click nút "Trang trước"
this.prevPageBtn.on('click', () => {
if (this.currentPage > 1) {
this.currentPage--;
this.loadImagesIntoModal(this.currentPage);
}
});
// Sự kiện click nút "Trang sau"
this.nextPageBtn.on('click', () => {
// Lấy tổng số trang từ thông tin hiện tại trên UI (đã được cập nhật từ AJAX)
let totalPages = parseInt(this.currentPageInfo.text().split('/')[1].trim());
if (this.currentPage < totalPages) {
this.currentPage++;
this.loadImagesIntoModal(this.currentPage);
}
});
// Sự kiện click vào ảnh trong lưới (sử dụng event delegation)
this.imageGridContent.on('click', 'img', (event) => {
const selectedImageUrl = $(event.currentTarget).data('image-url');
this.updateTargetImage(selectedImageUrl); // Cập nhật ảnh xem trước
// Gán URL ảnh đã chọn vào input link_image_thump
this.linkImageThumpInput.val(selectedImageUrl);
// Xóa giá trị của input type="file" nếu người dùng chuyển từ tải file sang chọn thư viện
this.fileInput.val('');
this.modal.css('display', 'none'); // Đóng modal
});
}
// Tải ảnh từ server vào modal với phân trang
loadImagesIntoModal(page) {
this.imageGridContent.html('<p>Đang tải ảnh...</p>'); // Hiển thị thông báo đang tải
$.ajax({
url: this.ajaxUrl,
type: 'GET',
dataType: 'json',
data: {
page: page,
limit: this.imagesPerPage
},
success: (response) => {
let imageGridHtml = '';
if (response.images && response.images.length > 0) {
response.images.forEach(function(imageUrl) {
imageGridHtml += `<img src="${imageUrl}" class="img-thumbnail" data-image-url="${imageUrl}">`;
});
} else {
imageGridHtml = '<p>Không có hình ảnh nào trên trang này.</p>';
}
this.imageGridContent.html(imageGridHtml); // Đổ HTML vào lưới ảnh
// Cập nhật thông tin phân trang
this.currentPageInfo.text(`Trang ${response.current_page} / ${response.total_pages}`);
// Cập nhật trạng thái (disabled) của các nút phân trang
this.prevPageBtn.prop('disabled', response.current_page <= 1);
this.nextPageBtn.prop('disabled', response.current_page >= response.total_pages);
// Nếu chỉ có 1 trang hoặc không có ảnh, vô hiệu hóa cả 2 nút
if (response.total_pages <= 1) {
this.prevPageBtn.prop('disabled', true);
this.nextPageBtn.prop('disabled', true);
}
},
error: (xhr, status, error) => {
console.error("Lỗi khi tải ảnh:", status, error);
this.imageGridContent.html('<p>Có lỗi khi tải hình ảnh.</p>');
// Vô hiệu hóa nút khi lỗi
this.prevPageBtn.prop('disabled', true);
this.nextPageBtn.prop('disabled', true);
this.currentPageInfo.text('Lỗi tải trang');
}
});
}
// Cập nhật thẻ img xem trước dựa trên mảng các ID ưu tiên
updateTargetImage(imageUrl) {
let found = false;
for (const id of this.targetImageReviewIds) {
const targetElement = $(`#${id}`);
if (targetElement.length > 0) { // Kiểm tra xem phần tử có tồn tại không
targetElement.attr('src', imageUrl);
found = true;
break; // Dừng lại ngay khi tìm thấy và cập nhật thẻ đầu tiên
}
}
if (!found) {
console.warn(`Không tìm thấy thẻ img với bất kỳ id nào trong [${this.targetImageReviewIds.join(', ')}] để cập nhật.`);
}
}
}
// Khởi tạo đối tượng ImageSelector khi DOM đã sẵn sàng
$(document).ready(function() {
const imageSelectorOptions = {
modalId: '#imageModal',
imageGridContentId: '#imageGridContent',
prevPageBtnId: '#prevPageBtn',
nextPageBtnId: '#nextPageBtn',
currentPageInfoId: '#currentPageInfo',
openModalBtnId: '#openImageModalBtn',
closeModalBtnId: '#closeImageModalBtn',
linkImageThumpInputId: '#link_image_thump', // ID của input ẩn
fileInputId: '#input_img', // ID của input type="file"
ajaxUrl: '<?php echo base_url('upload/get_images_paginated'); ?>', // URL AJAX để lấy ảnh
imagesPerPage: 60, // Số ảnh mỗi trang
targetImageReviewIds: ['noimage_review', 'image_review'] // Thứ tự ưu tiên các ID thẻ img xem trước
};
const myImageSelector = new ImageSelector(imageSelectorOptions);
});
</script>
</body>
</html>
---
base_url
trong application/config/config.php
đã được thiết lập chính xác.upload
: Tạo thư mục upload
trong thư mục gốc của dự án CodeIgniter và đảm bảo nó có quyền ghi. Đặt các ảnh vào đây (có thể cả trong thư mục con) để kiểm tra.application/controllers/Upload.php
bằng mã PHP đã cung cấp.application/views/upload_view_custom_modal_paginated.php
bằng toàn bộ mã HTML, CSS và JavaScript được cung cấp.http://localhost/your_ci_project/upload
(thay your_ci_project
bằng tên thư mục dự án của bạn).CKEDITOR.tools.callFunction()
để gửi dữ liệu trở lại.link_image_thump
): Input này rất hữu ích khi bạn gửi form chính chứa CKEditor. Server của bạn có thể đọc giá trị của link_image_thump
để biết người dùng đã chọn ảnh nào từ thư viện.