file manager popup rework 2
This commit is contained in:
@@ -152,6 +152,26 @@ ActiveAdmin.register_page "Files" do
|
||||
end
|
||||
end
|
||||
|
||||
# JSON API for AJAX file picker
|
||||
page_action :list, method: :get do
|
||||
service = FileManagerService.new
|
||||
dir = params[:dir].to_s.presence || ""
|
||||
begin
|
||||
entries = service.list(dir)
|
||||
render json: { entries: entries, dir: dir }
|
||||
rescue ArgumentError => e
|
||||
render json: { entries: [], dir: dir, error: e.message }
|
||||
end
|
||||
end
|
||||
|
||||
page_action :ajax_upload, method: :post do
|
||||
service = FileManagerService.new
|
||||
path = service.upload(params[:dir].to_s, params[:file])
|
||||
render json: { ok: true, path: path }
|
||||
rescue => e
|
||||
render json: { ok: false, error: e.message }, status: 422
|
||||
end
|
||||
|
||||
page_action :upload, method: :post do
|
||||
service = FileManagerService.new
|
||||
service.upload(params[:dir].to_s, params[:file])
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
(function() {
|
||||
var PATH_FIELDS = ['html_folder_path', 'cartridge_path', 'source_path', 'docs_folder_path'];
|
||||
var _pickerTarget = null;
|
||||
|
||||
function addBrowseButtons() {
|
||||
PATH_FIELDS.forEach(function(fieldName) {
|
||||
@@ -14,66 +15,207 @@
|
||||
btn.className = 'fm-browse-btn';
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
openFilePicker(input.id);
|
||||
_pickerTarget = input;
|
||||
openPicker('');
|
||||
});
|
||||
input.parentNode.insertBefore(btn, input.nextSibling);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openFilePicker(fieldId) {
|
||||
// Backdrop
|
||||
function openPicker(dir) {
|
||||
closePicker();
|
||||
var overlay = document.createElement('div');
|
||||
overlay.className = 'fm-modal-overlay';
|
||||
overlay.id = 'fm-picker-overlay';
|
||||
overlay.addEventListener('click', function(e) {
|
||||
if (e.target === overlay) closeFilePicker();
|
||||
if (e.target === overlay) closePicker();
|
||||
});
|
||||
|
||||
// Modal container
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'fm-modal';
|
||||
|
||||
// Close button
|
||||
var header = document.createElement('div');
|
||||
header.className = 'fm-modal-header';
|
||||
header.innerHTML = '<span class="fm-modal-title">Select file or folder</span>';
|
||||
var closeBtn = document.createElement('button');
|
||||
closeBtn.className = 'fm-modal-close';
|
||||
closeBtn.innerHTML = '×';
|
||||
closeBtn.addEventListener('click', closeFilePicker);
|
||||
modal.appendChild(closeBtn);
|
||||
closeBtn.addEventListener('click', closePicker);
|
||||
header.appendChild(closeBtn);
|
||||
modal.appendChild(header);
|
||||
|
||||
// Iframe
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.src = '/admin/files?picker=1&field=' + encodeURIComponent(fieldId);
|
||||
iframe.className = 'fm-modal-iframe';
|
||||
modal.appendChild(iframe);
|
||||
var body = document.createElement('div');
|
||||
body.className = 'fm-modal-body';
|
||||
body.id = 'fm-picker-body';
|
||||
body.innerHTML = '<div style="text-align:center;padding:40px;color:#999;">Loading...</div>';
|
||||
modal.appendChild(body);
|
||||
|
||||
overlay.appendChild(modal);
|
||||
document.body.appendChild(overlay);
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
window._fmOverlay = overlay;
|
||||
loadDir(dir);
|
||||
}
|
||||
|
||||
function closeFilePicker() {
|
||||
if (window._fmOverlay) {
|
||||
window._fmOverlay.remove();
|
||||
window._fmOverlay = null;
|
||||
document.body.style.overflow = '';
|
||||
function closePicker() {
|
||||
var ov = document.getElementById('fm-picker-overlay');
|
||||
if (ov) { ov.remove(); }
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
function selectPath(absPath) {
|
||||
if (_pickerTarget) {
|
||||
_pickerTarget.value = absPath;
|
||||
_pickerTarget.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
closePicker();
|
||||
}
|
||||
|
||||
function loadDir(dir) {
|
||||
var body = document.getElementById('fm-picker-body');
|
||||
if (!body) return;
|
||||
body.innerHTML = '<div style="text-align:center;padding:40px;color:#999;">Loading...</div>';
|
||||
|
||||
fetch('/admin/files/list?dir=' + encodeURIComponent(dir))
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) { renderDir(data.entries || [], data.dir || ''); })
|
||||
.catch(function(e) {
|
||||
body.innerHTML = '<div style="color:red;padding:20px;">Error: ' + e.message + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderDir(entries, dir) {
|
||||
var body = document.getElementById('fm-picker-body');
|
||||
if (!body) return;
|
||||
|
||||
var html = '';
|
||||
|
||||
// Breadcrumb
|
||||
var parts = dir ? dir.split('/').filter(function(p){return p;}) : [];
|
||||
html += '<div class="fm-breadcrumbs"><a href="#" data-dir="">softwares/</a>';
|
||||
var acc = '';
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
acc = acc ? acc + '/' + parts[i] : parts[i];
|
||||
html += '<a href="#" data-dir="' + esc(acc) + '">' + esc(parts[i]) + '/</a>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// Upload bar
|
||||
html += '<div class="fm-actions">';
|
||||
html += '<input type="file" id="fm-picker-file" />';
|
||||
html += '<button class="fm-btn" id="fm-picker-upload-btn">Upload</button>';
|
||||
html += '</div>';
|
||||
|
||||
// Table
|
||||
html += '<table class="fm-table"><thead><tr>';
|
||||
html += '<th style="width:50px">Type</th><th>Name</th><th style="width:90px">Size</th>';
|
||||
html += '<th style="width:80px"></th></tr></thead><tbody>';
|
||||
|
||||
// Parent dir
|
||||
if (dir) {
|
||||
var parentDir = dir.indexOf('/') >= 0 ? dir.substring(0, dir.lastIndexOf('/')) : '';
|
||||
html += '<tr><td class="fm-type">DIR</td>';
|
||||
html += '<td><a href="#" data-dir="' + esc(parentDir) + '">..</a></td>';
|
||||
html += '<td></td><td></td></tr>';
|
||||
}
|
||||
|
||||
for (var j = 0; j < entries.length; j++) {
|
||||
var e = entries[j];
|
||||
var absPath = '/softwares/' + e.path;
|
||||
html += '<tr>';
|
||||
html += '<td class="fm-type">' + (e.type === 'directory' ? 'DIR' : 'FILE') + '</td>';
|
||||
html += '<td>';
|
||||
if (e.type === 'directory') {
|
||||
html += '<a href="#" data-dir="' + esc(e.path) + '">' + esc(e.name) + '</a>';
|
||||
} else {
|
||||
html += esc(e.name);
|
||||
}
|
||||
html += '</td>';
|
||||
html += '<td>' + (e.size ? humanSize(e.size) : '') + '</td>';
|
||||
html += '<td><a href="#" class="fm-btn fm-btn-select" data-select="' + esc(absPath) + '">Select</a></td>';
|
||||
html += '</tr>';
|
||||
}
|
||||
|
||||
html += '</tbody></table>';
|
||||
body.innerHTML = html;
|
||||
|
||||
// Event delegation
|
||||
body.addEventListener('click', handlePickerClick);
|
||||
}
|
||||
|
||||
function handlePickerClick(e) {
|
||||
var t = e.target;
|
||||
|
||||
// Navigate directory
|
||||
if (t.tagName === 'A' && t.hasAttribute('data-dir')) {
|
||||
e.preventDefault();
|
||||
loadDir(t.getAttribute('data-dir'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Select file/folder
|
||||
if (t.hasAttribute('data-select')) {
|
||||
e.preventDefault();
|
||||
selectPath(t.getAttribute('data-select'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload button
|
||||
if (t.id === 'fm-picker-upload-btn') {
|
||||
e.preventDefault();
|
||||
var fileInput = document.getElementById('fm-picker-file');
|
||||
if (!fileInput || !fileInput.files.length) { alert('Select a file first'); return; }
|
||||
|
||||
var breadcrumbs = document.querySelector('#fm-picker-body .fm-breadcrumbs');
|
||||
var lastLink = breadcrumbs ? breadcrumbs.querySelectorAll('a') : [];
|
||||
var currentDir = lastLink.length ? lastLink[lastLink.length - 1].getAttribute('data-dir') || '' : '';
|
||||
|
||||
var csrfToken = document.querySelector('meta[name="csrf-token"]');
|
||||
var fd = new FormData();
|
||||
fd.append('file', fileInput.files[0]);
|
||||
fd.append('dir', currentDir);
|
||||
|
||||
t.textContent = 'Uploading...';
|
||||
t.disabled = true;
|
||||
|
||||
fetch('/admin/files/ajax_upload', {
|
||||
method: 'POST',
|
||||
headers: csrfToken ? { 'X-CSRF-Token': csrfToken.getAttribute('content') } : {},
|
||||
body: fd
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
loadDir(currentDir);
|
||||
} else {
|
||||
alert('Upload failed: ' + (data.error || 'unknown error'));
|
||||
t.textContent = 'Upload';
|
||||
t.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
alert('Upload error: ' + err.message);
|
||||
t.textContent = 'Upload';
|
||||
t.disabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Global callback for iframe to call directly (same-origin)
|
||||
window.filePickerSelect = function(fieldId, path) {
|
||||
var input = document.getElementById(fieldId);
|
||||
if (input) {
|
||||
input.value = path;
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
closeFilePicker();
|
||||
};
|
||||
function esc(s) {
|
||||
var d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function humanSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / 1048576).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', addBrowseButtons);
|
||||
|
||||
// ActiveAdmin has_many dynamic rows
|
||||
if (typeof jQuery !== 'undefined') {
|
||||
jQuery(document).on('has_many_add:after', function() {
|
||||
setTimeout(addBrowseButtons, 50);
|
||||
|
||||
@@ -39,12 +39,19 @@
|
||||
position: fixed; inset: 0; z-index: 9999; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.fm-modal {
|
||||
position: relative; width: 90vw; max-width: 1000px; height: 80vh; background: #fff; border-radius: 8px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); overflow: hidden;
|
||||
position: relative; width: 90vw; max-width: 1000px; height: 80vh; background: #fff; border-radius: 8px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); display: flex; flex-direction: column; overflow: hidden;
|
||||
}
|
||||
.fm-modal-header {
|
||||
display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid #e0e0e0; background: #f8f8f8; flex-shrink: 0;
|
||||
}
|
||||
.fm-modal-title { font-weight: bold; font-size: 15px; color: #333; }
|
||||
.fm-modal-close {
|
||||
position: absolute; top: 8px; right: 12px; z-index: 10; background: none; border: none; font-size: 28px; color: #666; cursor: pointer; line-height: 1;
|
||||
background: none; border: none; font-size: 24px; color: #666; cursor: pointer; line-height: 1; padding: 0 4px;
|
||||
&:hover { color: #000; }
|
||||
}
|
||||
.fm-modal-iframe {
|
||||
width: 100%; height: 100%; border: none;
|
||||
.fm-modal-body {
|
||||
flex: 1; overflow-y: auto; padding: 16px;
|
||||
.fm-breadcrumbs { margin-bottom: 12px; }
|
||||
.fm-actions { margin-bottom: 12px; }
|
||||
.fm-table { font-size: 13px; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user