Files
sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/Edit Page - SIS Vietnam - Manage_files/sis-media-picker.js.download
T

462 lines
25 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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">&times;</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);