diff options
Diffstat (limited to 'internal/admin/templates')
| -rw-r--r-- | internal/admin/templates/files.html | 130 | ||||
| -rw-r--r-- | internal/admin/templates/layout.html | 1 |
2 files changed, 120 insertions, 11 deletions
diff --git a/internal/admin/templates/files.html b/internal/admin/templates/files.html index b89a142..5a5106f 100644 --- a/internal/admin/templates/files.html +++ b/internal/admin/templates/files.html @@ -9,12 +9,14 @@ </div> </div> <script> -var currentPath = ''; +var currentDir = ''; +var currentFile = null; window._allFiles = []; function loadFiles(path) { - currentPath = path || ''; - API.get('/api/files' + (path ? '?path=' + encodeURIComponent(path) : '')).then(function(r){ + currentDir = path || ''; + currentFile = null; + return API.get('/api/files' + (path ? '?path=' + encodeURIComponent(path) : '')).then(function(r){ var files = r.files || r || []; window._allFiles = files; renderFileList(''); @@ -26,17 +28,17 @@ function renderFileList(filter){ var f = filter.toLowerCase(); var items = window._allFiles.filter(function(x){ return !f || x.name.toLowerCase().indexOf(f) >= 0; }); var html = '<div class="item" onclick="loadFiles(\'\')" style="color:#6cf">[root]</div>'; - if (currentPath) { - var parent = currentPath.split('/').slice(0,-1).join('/'); + if (currentDir) { + var parent = currentDir.split('/').slice(0,-1).join('/'); html += '<div class="item" onclick="loadFiles(\''+escJs(parent)+'\')" style="color:#6cf">[..]</div>'; } items.forEach(function(x){ if (x.dir) { - var sub = currentPath ? currentPath + '/' + x.name : x.name; + var sub = currentDir ? currentDir + '/' + x.name : x.name; html += '<div class="item" onclick="loadFiles(\''+escJs(sub)+'\')" style="color:#fc6">'+escHtml(x.name)+'/</div>'; } else { - var fp = currentPath ? currentPath + '/' + x.name : x.name; - html += '<div class="item" onclick="loadFileContent(\''+escJs(fp)+'\',\''+escJs(x.name)+'\')">'+escHtml(x.name)+'</div>'; + var fp = currentDir ? currentDir + '/' + x.name : x.name; + html += '<div class="item" data-path="'+escHtml(fp)+'" onclick="loadFileContent(\''+escJs(fp)+'\',\''+escJs(x.name)+'\')" oncontextmenu="showFileContextMenu(event,\''+escJs(fp)+'\',\''+escJs(x.name)+'\')">'+escHtml(x.name)+'</div>'; } }); el.innerHTML = html; @@ -45,7 +47,7 @@ function renderFileList(filter){ function filterFiles(val) { renderFileList(val); } function loadFileContent(path, name) { - currentPath = path; + currentFile = path; API.get('/api/files?path=' + encodeURIComponent(path)).then(function(r){ var content = r.content || r.raw || JSON.stringify(r,null,2); var html = '<h2>'+escHtml(name)+'</h2>'; @@ -66,13 +68,119 @@ function saveFile(path) { }).then(function(r) { if (!r.ok) throw new Error('Save failed'); return r.json(); - }).then(function() { + }).then(function(data) { + if (data && data.error) throw new Error(data.error); notify('File saved', 'success'); updateUndoBar(); }).catch(function(e) { notify('Save failed: '+e.message, 'error'); }); } -window.refreshPage = function() { loadFiles(currentPath); }; +function showFileContextMenu(e, path, name) { + openContextMenu(e, [ + {label: 'Rename', handler: function() { startInlineFileRename(path, name); }}, + {label: 'Duplicate', handler: function() { duplicateFile(path, name); }}, + {label: 'Delete', cls: 'danger', handler: function() { deleteFile(path); }}, + ]); +} + +function deleteFile(path) { + API.del('/api/files?path=' + encodeURIComponent(path)).then(function(r) { + notify('File deleted', 'success'); + updateUndoBar(); + if (currentFile === path) { + $('#editorMain').innerHTML = '<p style="color:#888;text-align:center;margin-top:60px">Select a file to edit</p>'; + currentFile = null; + } + loadFiles(currentDir); + }).catch(function(e) { notify('Delete failed: ' + e.message, 'error'); }); +} + +function duplicateFile(path, name) { + API.get('/api/files?path=' + encodeURIComponent(path)).then(function(r) { + var content = r.content || ''; + var dot = name.lastIndexOf('.'); + var base, ext; + if (dot > 0) { base = name.substring(0, dot); ext = name.substring(dot); } + else { base = name; ext = ''; } + + var baseName = base + '_copy'; + var newName = baseName + ext; + var counter = 2; + while (true) { + var exists = false; + for (var i = 0; i < window._allFiles.length; i++) { + if (window._allFiles[i].name === newName) { exists = true; break; } + } + if (!exists) break; + newName = baseName + '_' + counter + ext; + counter++; + } + + var newPath = currentDir ? currentDir + '/' + newName : newName; + fetch('/api/files?path=' + encodeURIComponent(newPath), { + method: 'PUT', + headers: {'Content-Type': 'text/plain'}, + body: content, + credentials: 'same-origin' + }).then(function(r) { + if (!r.ok) throw new Error('Duplicate failed'); + return r.json(); + }).then(function(data) { + if (data && data.error) throw new Error(data.error); + notify('Duplicated as ' + newName, 'success'); + updateUndoBar(); + loadFiles(currentDir).then(function() { loadFileContent(newPath, newName); }); + }).catch(function(e) { notify('Duplicate failed: ' + e.message, 'error'); }); + }).catch(function(e) { notify('Duplicate failed: ' + e.message, 'error'); }); +} + +function startInlineFileRename(path, name) { + var el = document.querySelector('#listEntries .item[data-path="' + escHtml(path) + '"]'); + if (!el) return; + var origHTML = el.innerHTML; + el.innerHTML = '<input class="item-rename-input" value="' + escHtml(name) + '" maxlength="255">'; + var input = el.querySelector('.item-rename-input'); + input.focus(); + input.select(); + + function finish(val) { + val = (val || '').trim(); + if (!val || val === name) { el.innerHTML = origHTML; return; } + if (val.indexOf('/') >= 0 || val.indexOf('\\') >= 0 || val === '.' || val === '..') { + notify('Invalid file name', 'error'); + el.innerHTML = origHTML; + return; + } + for (var i = 0; i < val.length; i++) { + var c = val.charCodeAt(i); + if (c < 32 || c === 127) { notify('Invalid file name', 'error'); el.innerHTML = origHTML; return; } + } + for (var j = 0; j < window._allFiles.length; j++) { + if (window._allFiles[j].name === val) { + notify('A file named "' + val + '" already exists.', 'error'); + el.innerHTML = origHTML; + return; + } + } + API.post('/api/files/rename', {path: path, new_name: val}).then(function(r) { + var newPath = r.path || (currentDir ? currentDir + '/' + val : val); + notify('Renamed to ' + val, 'success'); + updateUndoBar(); + loadFiles(currentDir).then(function() { loadFileContent(newPath, val); }); + }).catch(function(e) { + notify('Rename failed: ' + e.message, 'error'); + el.innerHTML = origHTML; + }); + } + + input.onkeydown = function(e) { + if (e.key === 'Enter') { input.blur(); } + else if (e.key === 'Escape') { el.innerHTML = origHTML; } + }; + input.onblur = function() { finish(input.value); }; +} + +window.refreshPage = function() { loadFiles(currentDir); }; function escHtml(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); } function escJs(s) { return String(s||'').replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/"/g,'\\"'); } diff --git a/internal/admin/templates/layout.html b/internal/admin/templates/layout.html index fe4e0f9..f6bd405 100644 --- a/internal/admin/templates/layout.html +++ b/internal/admin/templates/layout.html @@ -6,6 +6,7 @@ <title>THOI Admin{{if .Page}} — {{.Page}}{{end}}</title> <link rel="stylesheet" href="/static/admin.css"> <script src="/static/admin.js"></script> +<script src="/static/ctxmenu.js"></script> <script src="/static/colorpicker.js"></script> </head> <body> |
