462 lines
25 KiB
Plaintext
462 lines
25 KiB
Plaintext
/**
|
||
* SIS Media Picker & Library Modal Utility
|
||
* Standalone reusable module to select and upload media files anywhere in the application.
|
||
*
|
||
* Usage (simple - just URL returned):
|
||
* window.openSISMediaModal(function(selectedUrl, selectedMediaObj) {
|
||
* console.log("Selected Image URL:", selectedUrl);
|
||
* });
|
||
*
|
||
* Usage (extended - returns URL + config object with style, attributes, size):
|
||
* SISMediaPicker.open(function(selectedUrl, selectedMediaObj, config) {
|
||
* // config = { style, customAttributes, width, height }
|
||
* });
|
||
*
|
||
* Or via object API:
|
||
* SISMediaPicker.open(callback, options);
|
||
* options = { showConfig: true } // default: true
|
||
*/
|
||
(function (window, document) {
|
||
'use strict';
|
||
|
||
var PREDEFINED_SIZES = [
|
||
{ label: 'Auto', width: '', height: '' },
|
||
{ label: '25%', width: '25%', height: '' },
|
||
{ label: '50%', width: '50%', height: '' },
|
||
{ label: '75%', width: '75%', height: '' },
|
||
{ label: '100%', width: '100%', height: '' },
|
||
{ label: '100px', width: '100px', height: '100px' },
|
||
{ label: '150px', width: '150px', height: '150px' },
|
||
{ label: '200px', width: '200px', height: '200px' },
|
||
{ label: '300px', width: '300px', height: 'auto' },
|
||
{ label: '400px', width: '400px', height: 'auto' },
|
||
{ label: '500px', width: '500px', height: 'auto' },
|
||
{ label: 'Full Width', width: '100%', height: 'auto' },
|
||
{ label: 'Thumbnail (80×80)', width: '80px', height: '80px' },
|
||
{ label: 'Avatar (120×120)', width: '120px', height: '120px' },
|
||
{ label: 'Banner (1200×400)', width: '1200px', height: '400px' },
|
||
];
|
||
|
||
var SISMediaPicker = {
|
||
callback: null,
|
||
mediaData: [],
|
||
modalId: 'sisMediaLibraryModal',
|
||
configPanelId: 'sisMediaConfigPanel',
|
||
selectedUrl: null,
|
||
selectedMediaObj: null,
|
||
|
||
initModal: function () {
|
||
var modalEl = document.getElementById(this.modalId);
|
||
if (modalEl) return modalEl;
|
||
|
||
modalEl = document.createElement('div');
|
||
modalEl.id = this.modalId;
|
||
modalEl.className = 'modal fade';
|
||
modalEl.setAttribute('tabindex', '-1');
|
||
modalEl.setAttribute('role', 'dialog');
|
||
modalEl.setAttribute('aria-hidden', 'true');
|
||
modalEl.innerHTML =
|
||
'<div class="modal-dialog modal-xl" role="document">' +
|
||
'<div class="modal-content border-0 shadow-lg">' +
|
||
'<div class="modal-header bg-primary text-white p-3">' +
|
||
'<h5 class="modal-title font-weight-bold" id="' + this.modalId + 'Label">' +
|
||
'<i class="fas fa-images mr-2"></i> Thư viện Media / Chọn Hình ảnh' +
|
||
'</h5>' +
|
||
'<button type="button" class="close text-white opacity-100" data-dismiss="modal" aria-label="Close">' +
|
||
'<span aria-hidden="true">×</span>' +
|
||
'</button>' +
|
||
'</div>' +
|
||
'<div class="modal-body bg-light p-3">' +
|
||
/* Toolbar row */
|
||
'<div class="d-flex flex-wrap justify-content-between align-items-center mb-3 bg-white p-2 border rounded shadow-sm">' +
|
||
'<div class="input-group input-group-sm mb-2 mb-md-0" style="max-width: 380px;">' +
|
||
'<div class="input-group-prepend"><span class="input-group-text bg-light"><i class="fas fa-search text-muted"></i></span></div>' +
|
||
'<input type="text" class="form-control" id="sisMediaSearchInput" placeholder="Tìm kiếm theo tên file..." />' +
|
||
'</div>' +
|
||
'<div>' +
|
||
'<label class="btn btn-sm btn-success mb-0 cursor-pointer shadow-sm" style="cursor: pointer;">' +
|
||
'<i class="fas fa-cloud-upload-alt mr-1"></i> Tải ảnh mới lên' +
|
||
'<input type="file" id="sisMediaUploadInput" accept="image/*" style="display: none;" />' +
|
||
'</label>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
/* Media Grid */
|
||
'<div id="sisMediaGrid" class="row" style="max-height: 380px; overflow-y: auto; align-content: start;">' +
|
||
'<div class="col-12 text-center text-muted py-5"><i class="fas fa-spinner fa-spin fa-2x"></i><br/><span class="mt-2 d-block">Đang tải danh sách hình ảnh...</span></div>' +
|
||
'</div>' +
|
||
/* Config Panel - hidden until an image is selected */
|
||
'<div id="' + this.configPanelId + '" class="mt-3 p-3 bg-white border rounded shadow-sm" style="display: none;">' +
|
||
'<h6 class="font-weight-bold text-primary mb-3"><i class="fas fa-sliders-h mr-1"></i> Cấu hình hình ảnh đã chọn</h6>' +
|
||
'<div class="row">' +
|
||
/* Preview */
|
||
'<div class="col-md-3 text-center mb-3">' +
|
||
'<p class="small text-muted mb-1 font-weight-bold">Xem trước</p>' +
|
||
'<div class="border rounded p-2 bg-light" style="min-height: 100px; display: flex; align-items: center; justify-content: center;">' +
|
||
'<img id="sisMediaConfigPreview" src="" alt="Preview" style="max-width: 100%; max-height: 150px; object-fit: contain;" />' +
|
||
'</div>' +
|
||
'<p id="sisMediaConfigName" class="small text-truncate mt-1 text-muted mb-0"></p>' +
|
||
'</div>' +
|
||
/* Config Fields */
|
||
'<div class="col-md-9">' +
|
||
'<div class="row">' +
|
||
/* Predefined Sizes */
|
||
'<div class="col-12 mb-2">' +
|
||
'<label class="small font-weight-bold text-muted d-block mb-1" style="font-size: 10px; text-transform: uppercase;">Kích thước nhanh</label>' +
|
||
'<div id="sisMediaSizeButtons" class="d-flex flex-wrap gap-1" style="gap: 4px;">' +
|
||
'</div>' +
|
||
'</div>' +
|
||
/* Width */
|
||
'<div class="col-md-4 mb-2">' +
|
||
'<label class="small font-weight-bold text-muted mb-1" style="font-size: 10px;">Chiều rộng (width)</label>' +
|
||
'<input type="text" id="sisMediaConfigWidth" class="form-control form-control-sm" placeholder="e.g. 100%, 300px, auto" />' +
|
||
'</div>' +
|
||
/* Height */
|
||
'<div class="col-md-4 mb-2">' +
|
||
'<label class="small font-weight-bold text-muted mb-1" style="font-size: 10px;">Chiều cao (height)</label>' +
|
||
'<input type="text" id="sisMediaConfigHeight" class="form-control form-control-sm" placeholder="e.g. auto, 200px" />' +
|
||
'</div>' +
|
||
/* Object Fit */
|
||
'<div class="col-md-4 mb-2">' +
|
||
'<label class="small font-weight-bold text-muted mb-1" style="font-size: 10px;">Object Fit</label>' +
|
||
'<select id="sisMediaConfigObjectFit" class="form-control form-control-sm">' +
|
||
'<option value="">-- default --</option>' +
|
||
'<option value="cover">cover</option>' +
|
||
'<option value="contain">contain</option>' +
|
||
'<option value="fill">fill</option>' +
|
||
'<option value="none">none</option>' +
|
||
'<option value="scale-down">scale-down</option>' +
|
||
'</select>' +
|
||
'</div>' +
|
||
/* Aspect Ratio */
|
||
'<div class="col-md-4 mb-2">' +
|
||
'<label class="small font-weight-bold text-muted mb-1" style="font-size: 10px;">Aspect Ratio</label>' +
|
||
'<select id="sisMediaConfigAspectRatio" class="form-control form-control-sm">' +
|
||
'<option value="">-- none --</option>' +
|
||
'<option value="1/1">1:1 (Square)</option>' +
|
||
'<option value="4/3">4:3 (Standard)</option>' +
|
||
'<option value="16/9">16:9 (Widescreen)</option>' +
|
||
'<option value="21/9">21:9 (Ultrawide)</option>' +
|
||
'<option value="3/4">3:4 (Portrait)</option>' +
|
||
'<option value="9/16">9:16 (Vertical Video)</option>' +
|
||
'<option value="2/1">2:1 (Panorama)</option>' +
|
||
'<option value="3/2">3:2 (Photo)</option>' +
|
||
'<option value="5/4">5:4</option>' +
|
||
'</select>' +
|
||
'</div>' +
|
||
/* CSS Class */
|
||
'<div class="col-md-4 mb-2">' +
|
||
'<label class="small font-weight-bold text-muted mb-1" style="font-size: 10px;">CSS Class</label>' +
|
||
'<input type="text" id="sisMediaConfigClass" class="form-control form-control-sm" placeholder="e.g. img-fluid rounded shadow" />' +
|
||
'</div>' +
|
||
/* Inline Style */
|
||
'<div class="col-md-6 mb-2">' +
|
||
'<label class="small font-weight-bold text-muted mb-1" style="font-size: 10px;">Inline Style (CSS)</label>' +
|
||
'<input type="text" id="sisMediaConfigStyle" class="form-control form-control-sm" placeholder="e.g. border-radius: 8px; opacity: 0.9;" />' +
|
||
'</div>' +
|
||
/* Alt Text */
|
||
'<div class="col-md-6 mb-2">' +
|
||
'<label class="small font-weight-bold text-muted mb-1" style="font-size: 10px;">Alt Text</label>' +
|
||
'<input type="text" id="sisMediaConfigAlt" class="form-control form-control-sm" placeholder="Image description..." />' +
|
||
'</div>' +
|
||
/* Custom Attributes */
|
||
'<div class="col-md-6 mb-2">' +
|
||
'<label class="small font-weight-bold text-muted mb-1" style="font-size: 10px;">Custom Attributes (HTML)</label>' +
|
||
'<input type="text" id="sisMediaConfigAttrs" class="form-control form-control-sm" placeholder=\'e.g. data-id="5" loading="lazy"\' />' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="text-right mt-2">' +
|
||
'<button type="button" class="btn btn-sm btn-secondary mr-2" id="sisMediaConfigCancel">Hủy</button>' +
|
||
'<button type="button" class="btn btn-sm btn-primary" id="sisMediaConfigConfirm"><i class="fas fa-check mr-1"></i> Chọn ảnh này</button>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="modal-footer bg-white p-2">' +
|
||
'<button type="button" class="btn btn-sm btn-secondary px-3" data-dismiss="modal">Đóng</button>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>';
|
||
|
||
document.body.appendChild(modalEl);
|
||
|
||
// Build predefined size buttons
|
||
var sizeContainer = document.getElementById('sisMediaSizeButtons');
|
||
if (sizeContainer) {
|
||
PREDEFINED_SIZES.forEach(function (size) {
|
||
var btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'btn btn-xs btn-outline-secondary mb-1';
|
||
btn.style.fontSize = '10px';
|
||
btn.style.padding = '1px 6px';
|
||
btn.style.marginRight = '3px';
|
||
btn.innerText = size.label;
|
||
btn.addEventListener('click', function () {
|
||
document.getElementById('sisMediaConfigWidth').value = size.width;
|
||
document.getElementById('sisMediaConfigHeight').value = size.height;
|
||
sizeContainer.querySelectorAll('button').forEach(function (b) {
|
||
b.classList.remove('btn-secondary');
|
||
b.classList.add('btn-outline-secondary');
|
||
});
|
||
btn.classList.remove('btn-outline-secondary');
|
||
btn.classList.add('btn-secondary');
|
||
});
|
||
sizeContainer.appendChild(btn);
|
||
});
|
||
}
|
||
|
||
// Bind Search Input Handler
|
||
var self = this;
|
||
document.addEventListener('input', function (e) {
|
||
if (e.target && e.target.id === 'sisMediaSearchInput') {
|
||
var term = e.target.value.toLowerCase().trim();
|
||
var filtered = self.mediaData.filter(function (item) {
|
||
var name = (item.originalFilename || item.name || '').toLowerCase();
|
||
return name.includes(term);
|
||
});
|
||
self.renderGrid(filtered);
|
||
}
|
||
});
|
||
|
||
// Bind Upload Input Handler
|
||
document.addEventListener('change', function (e) {
|
||
if (e.target && e.target.id === 'sisMediaUploadInput') {
|
||
var file = e.target.files[0];
|
||
if (!file) return;
|
||
|
||
var formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
var grid = document.getElementById('sisMediaGrid');
|
||
if (grid) {
|
||
grid.innerHTML = '<div class="col-12 text-center text-info py-5"><i class="fas fa-spinner fa-spin fa-2x"></i><br/><span class="mt-2 d-block">Đang tải tệp lên server...</span></div>';
|
||
}
|
||
|
||
fetch('/api/manage/media/upload', {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(function (res) { return res.json(); })
|
||
.then(function (data) {
|
||
if (data && data.success === 1 && data.file && data.file.url) {
|
||
self.showConfigPanel(data.file.url, data.file);
|
||
} else {
|
||
alert('Tải tệp lên thất bại. Vui lòng thử lại!');
|
||
self.fetchMediaList();
|
||
}
|
||
})
|
||
.catch(function (err) {
|
||
alert('Lỗi tải tệp: ' + err.message);
|
||
self.fetchMediaList();
|
||
});
|
||
}
|
||
});
|
||
|
||
// Config panel Cancel button
|
||
document.addEventListener('click', function (e) {
|
||
if (e.target && e.target.id === 'sisMediaConfigCancel') {
|
||
self.hideConfigPanel();
|
||
}
|
||
if (e.target && e.target.id === 'sisMediaConfigConfirm') {
|
||
self.confirmWithConfig();
|
||
}
|
||
});
|
||
|
||
return modalEl;
|
||
},
|
||
|
||
open: function (callback) {
|
||
this.callback = callback;
|
||
var modalEl = this.initModal();
|
||
|
||
if (typeof $ !== 'undefined' && $.fn && $.fn.modal) {
|
||
$(modalEl).modal('show');
|
||
} else {
|
||
modalEl.style.display = 'block';
|
||
modalEl.classList.add('show');
|
||
}
|
||
this.fetchMediaList();
|
||
},
|
||
|
||
fetchMediaList: function () {
|
||
var grid = document.getElementById('sisMediaGrid');
|
||
if (grid) {
|
||
grid.innerHTML = '<div class="col-12 text-center text-muted py-5"><i class="fas fa-spinner fa-spin fa-2x text-primary"></i><br/><span class="mt-2 d-block small">Đang nạp thư viện...</span></div>';
|
||
}
|
||
this.hideConfigPanel();
|
||
|
||
var self = this;
|
||
fetch('/api/manage/media/list')
|
||
.then(function (res) { return res.json(); })
|
||
.then(function (data) {
|
||
self.mediaData = data || [];
|
||
self.renderGrid(self.mediaData);
|
||
})
|
||
.catch(function (err) {
|
||
if (grid) {
|
||
grid.innerHTML = '<div class="col-12 text-center text-danger py-4"><i class="fas fa-exclamation-triangle fa-2x mb-2"></i><br/>Không thể tải danh sách tệp. Bạn có thể dán đường dẫn URL trực tiếp.</div>';
|
||
}
|
||
});
|
||
},
|
||
|
||
renderGrid: function (list) {
|
||
var grid = document.getElementById('sisMediaGrid');
|
||
if (!grid) return;
|
||
|
||
if (!list || list.length === 0) {
|
||
grid.innerHTML = '<div class="col-12 text-center text-muted py-5"><i class="far fa-folder-open fa-2x mb-2"></i><br/>Chưa có tệp hình ảnh nào. Bấm nút "Tải ảnh mới lên" để thêm.</div>';
|
||
return;
|
||
}
|
||
|
||
var self = this;
|
||
grid.innerHTML = list.map(function (item) {
|
||
var url = item.fileUrl || item.url || '';
|
||
var name = item.originalFilename || item.name || 'Image';
|
||
return '<div class="col-lg-2 col-md-3 col-sm-4 col-6 mb-3">' +
|
||
'<div class="card h-100 border shadow-sm media-select-card" style="cursor: pointer; transition: all 0.2s;" data-url="' + url + '" data-name="' + name + '">' +
|
||
'<div style="height: 110px; background: #f8f9fc; display: flex; align-items: center; justify-content: center; overflow: hidden;" class="p-1">' +
|
||
'<img src="' + url + '" style="max-height: 100%; max-width: 100%; object-fit: contain;" alt="' + name + '" />' +
|
||
'</div>' +
|
||
'<div class="card-body p-2 text-center bg-white border-top">' +
|
||
'<p class="card-text small text-truncate m-0 font-weight-bold text-dark" title="' + name + '">' + name + '</p>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>';
|
||
}).join('');
|
||
|
||
// Add click listeners to items
|
||
grid.querySelectorAll('.media-select-card').forEach(function (card) {
|
||
card.addEventListener('click', function () {
|
||
var selectedUrl = card.getAttribute('data-url');
|
||
var selectedName = card.getAttribute('data-name');
|
||
// Highlight selected
|
||
grid.querySelectorAll('.media-select-card').forEach(function (c) {
|
||
c.style.outline = '';
|
||
});
|
||
card.style.outline = '3px solid #007bff';
|
||
self.showConfigPanel(selectedUrl, { name: selectedName, url: selectedUrl });
|
||
});
|
||
});
|
||
},
|
||
|
||
showConfigPanel: function (url, mediaObj) {
|
||
this.selectedUrl = url;
|
||
this.selectedMediaObj = mediaObj || {};
|
||
|
||
var panel = document.getElementById(this.configPanelId);
|
||
if (!panel) return;
|
||
|
||
// Populate preview
|
||
var previewImg = document.getElementById('sisMediaConfigPreview');
|
||
if (previewImg) previewImg.src = url;
|
||
|
||
var nameEl = document.getElementById('sisMediaConfigName');
|
||
if (nameEl) nameEl.textContent = mediaObj && (mediaObj.originalFilename || mediaObj.name) ? (mediaObj.originalFilename || mediaObj.name) : '';
|
||
|
||
// Reset all config fields
|
||
var fields = ['sisMediaConfigWidth', 'sisMediaConfigHeight', 'sisMediaConfigStyle', 'sisMediaConfigClass', 'sisMediaConfigAlt', 'sisMediaConfigAttrs'];
|
||
var selectFields = ['sisMediaConfigObjectFit', 'sisMediaConfigAspectRatio'];
|
||
selectFields.forEach(function (id) { var el = document.getElementById(id); if (el) el.value = ''; });
|
||
fields.forEach(function (id) {
|
||
var el = document.getElementById(id);
|
||
if (el) el.value = '';
|
||
});
|
||
var fitEl = document.getElementById('sisMediaConfigObjectFit');
|
||
if (fitEl) fitEl.value = '';
|
||
|
||
// Reset size button highlights
|
||
var sizeContainer = document.getElementById('sisMediaSizeButtons');
|
||
if (sizeContainer) {
|
||
sizeContainer.querySelectorAll('button').forEach(function (b) {
|
||
b.classList.remove('btn-secondary');
|
||
b.classList.add('btn-outline-secondary');
|
||
});
|
||
}
|
||
|
||
panel.style.display = 'block';
|
||
panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||
},
|
||
|
||
hideConfigPanel: function () {
|
||
var panel = document.getElementById(this.configPanelId);
|
||
if (panel) panel.style.display = 'none';
|
||
this.selectedUrl = null;
|
||
this.selectedMediaObj = null;
|
||
},
|
||
|
||
confirmWithConfig: function () {
|
||
if (!this.selectedUrl) return;
|
||
|
||
var width = (document.getElementById('sisMediaConfigWidth') || {}).value || '';
|
||
var height = (document.getElementById('sisMediaConfigHeight') || {}).value || '';
|
||
var objectFit = (document.getElementById('sisMediaConfigObjectFit') || {}).value || '';
|
||
var aspectRatio = (document.getElementById('sisMediaConfigAspectRatio') || {}).value || '';
|
||
var style = (document.getElementById('sisMediaConfigStyle') || {}).value || '';
|
||
var cssClass = (document.getElementById('sisMediaConfigClass') || {}).value || '';
|
||
var alt = (document.getElementById('sisMediaConfigAlt') || {}).value || '';
|
||
var customAttrs = (document.getElementById('sisMediaConfigAttrs') || {}).value || '';
|
||
|
||
// Build composite style string from size + aspect-ratio + object-fit + inline style
|
||
var computedStyle = '';
|
||
if (width) computedStyle += 'width: ' + width + '; ';
|
||
if (height) computedStyle += 'height: ' + height + '; ';
|
||
if (aspectRatio) computedStyle += 'aspect-ratio: ' + aspectRatio + '; ';
|
||
if (objectFit) computedStyle += 'object-fit: ' + objectFit + '; ';
|
||
if (style) computedStyle += style;
|
||
computedStyle = computedStyle.trim();
|
||
|
||
var config = {
|
||
style: computedStyle,
|
||
cssClass: cssClass,
|
||
alt: alt,
|
||
customAttributes: customAttrs,
|
||
width: width,
|
||
height: height,
|
||
objectFit: objectFit,
|
||
aspectRatio: aspectRatio
|
||
};
|
||
|
||
if (typeof this.callback === 'function') {
|
||
this.callback(this.selectedUrl, this.selectedMediaObj, config);
|
||
}
|
||
|
||
// Close modal
|
||
var modalEl = document.getElementById(this.modalId);
|
||
if (modalEl) {
|
||
if (typeof $ !== 'undefined' && $.fn && $.fn.modal) {
|
||
$(modalEl).modal('hide');
|
||
} else {
|
||
modalEl.style.display = 'none';
|
||
modalEl.classList.remove('show');
|
||
}
|
||
}
|
||
this.hideConfigPanel();
|
||
},
|
||
|
||
selectItem: function (url, mediaObj) {
|
||
// Legacy direct select (no config panel) - used by upload flow or external callers
|
||
if (typeof this.callback === 'function') {
|
||
this.callback(url, mediaObj, {});
|
||
}
|
||
var modalEl = document.getElementById(this.modalId);
|
||
if (modalEl) {
|
||
if (typeof $ !== 'undefined' && $.fn && $.fn.modal) {
|
||
$(modalEl).modal('hide');
|
||
} else {
|
||
modalEl.style.display = 'none';
|
||
modalEl.classList.remove('show');
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// Expose standalone module globally
|
||
window.SISMediaPicker = SISMediaPicker;
|
||
|
||
// Backward-compatible global helper function
|
||
window.openSISMediaModal = function (callback) {
|
||
SISMediaPicker.open(callback);
|
||
};
|
||
|
||
})(window, document);
|