/** * 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 = ''; 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 = '

Đang tải tệp lên server...
'; } 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 = '

Đang nạp thư viện...
'; } 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 = '

Không thể tải danh sách tệp. Bạn có thể dán đường dẫn URL trực tiếp.
'; } }); }, renderGrid: function (list) { var grid = document.getElementById('sisMediaGrid'); if (!grid) return; if (!list || list.length === 0) { grid.innerHTML = '

Chưa có tệp hình ảnh nào. Bấm nút "Tải ảnh mới lên" để thêm.
'; return; } var self = this; grid.innerHTML = list.map(function (item) { var url = item.fileUrl || item.url || ''; var name = item.originalFilename || item.name || 'Image'; return '
' + '
' + '
' + '' + name + '' + '
' + '
' + '

' + name + '

' + '
' + '
' + '
'; }).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);