mirror of
https://github.com/esp8266/Arduino.git
synced 2025-04-21 10:26:06 +03:00
* Minimal file with a few ESP8266-specific keywords - github issue #3701 * Renamed "SDWebServer" to the more universal "WebFileManager" * SD was replaced by SDFS, and sketch now works on either SDFS, SPIFFS or LittleFS based on a #define logic (required adding a second param to open() and replacing 'FILE_WRITE' by "w") + Added size information to file list and a /status request handler to return filesystem status * Tree panel width is now proportional to window. Changed icons (lighter and more neutral), including one for files. Show size of files. Fill "filename" box upon clicking on a file. Sort files alphabetically. * Replaced by a lighter version * Return the filesystem time in the status object + Massive cleanup/merge/align with some code from the FSBrowser example and misc refactorings * Fixed folder handling * Replaced the FILESYSTEM #define by a filesystem variable, and introduced FSConfig to prevent FS formating. Fixed recursive deletion. Got rid of specific isDir() for SPIFFS. * Made 8.3 lowercase filenames formating optional (disabled by default). Refresh only part of the tree when possible. Selecting a file for upload defaults to the same folder as the last clicked file. Removed the Mkdir button on SPIFFS. * Added 'wait' cursor during asynchronous operations. Slight refactoring of XMLHttpRequest completion handling * Removed limitation "files must have an extension, folders may not". Case insensivity of the extension for the editor and preview. * Support Filenames without extension, Dirnames with extension. Added Save/Discard/Help buttons to Editor, discard confirmation on leave, and refresh tree/status upon save. Removed redundant Ctrl-Z + Ctrl-Shift-Z shortcut declarations. Small bug fixes. + some refactoring * Fixed tree refresh on delete in all cases by returning the remaining path as response to the delete request. Refactoring * Changed FS status in text by a percentage graph, with numbers as tooltip. Unsupported files on SPIFFS (files at root not sarting with "/", files with double "/", files ending with "/") are now detected and reported in the page. * Small fix + refactoring * Restrict filename support check to SPIFFS. * Implemented Move/Rename. Added "loading" screen during async operations (dim with spinner and status). Fixed "discard" feature that kept prompting even after an image was loaded. Improved refresh of parts of the tree, with recursive listing. Moved the "path" id attribute to the "li" elements for folders (was already the case for files). Refactoring and cleanup. * Fixed broken spinner * Cosmetic improvements. Removed non-functional Upload context menu. Fixed error in response to move requests. Added minified version. * Added specific icons for text and image files. Fixed incompatibilities with SPIFFS. Fixed a race condition between deletion and reinsertion of nodes when multiple folders are refreshed. Fixed missing URL decoding for files with special chars (e.g. space char). Moved info from source code comment to a readme.md file. Added source PNG to git. Cleanup. * Added favicon.ico. * Renamed project * Small changes * Add a note about the ace.js dependency * Minor changes * Define LittleFS by default. If both uncompressed and gz versions exist, use uncompressed version. Small fixes. * Define LittleFS by default. If both uncompressed and gz versions exist, use uncompressed version. Small fixes. * Restyled version * (dummy edit to retrigger broken CI) * Using unsigned int for comparison with String.length() * Return an error when upload fails (e.g. filesystem full) * Trying to reorder functions to please CI * Reordered functions to please CI. * Moved file * Renamed "SDWebServer" to the more universal "WebFileManager" * SD was replaced by SDFS, and sketch now works on either SDFS, SPIFFS or LittleFS based on a #define logic (required adding a second param to open() and replacing 'FILE_WRITE' by "w") + Added size information to file list and a /status request handler to return filesystem status * Tree panel width is now proportional to window. Changed icons (lighter and more neutral), including one for files. Show size of files. Fill "filename" box upon clicking on a file. Sort files alphabetically. * Replaced by a lighter version * Return the filesystem time in the status object + Massive cleanup/merge/align with some code from the FSBrowser example and misc refactorings * Fixed folder handling * Replaced the FILESYSTEM #define by a filesystem variable, and introduced FSConfig to prevent FS formating. Fixed recursive deletion. Got rid of specific isDir() for SPIFFS. * Made 8.3 lowercase filenames formating optional (disabled by default). Refresh only part of the tree when possible. Selecting a file for upload defaults to the same folder as the last clicked file. Removed the Mkdir button on SPIFFS. * Added 'wait' cursor during asynchronous operations. Slight refactoring of XMLHttpRequest completion handling * Removed limitation "files must have an extension, folders may not". Case insensivity of the extension for the editor and preview. * Support Filenames without extension, Dirnames with extension. Added Save/Discard/Help buttons to Editor, discard confirmation on leave, and refresh tree/status upon save. Removed redundant Ctrl-Z + Ctrl-Shift-Z shortcut declarations. Small bug fixes. + some refactoring * Fixed tree refresh on delete in all cases by returning the remaining path as response to the delete request. Refactoring * Changed FS status in text by a percentage graph, with numbers as tooltip. Unsupported files on SPIFFS (files at root not sarting with "/", files with double "/", files ending with "/") are now detected and reported in the page. * Small fix + refactoring * Restrict filename support check to SPIFFS. * Implemented Move/Rename. Added "loading" screen during async operations (dim with spinner and status). Fixed "discard" feature that kept prompting even after an image was loaded. Improved refresh of parts of the tree, with recursive listing. Moved the "path" id attribute to the "li" elements for folders (was already the case for files). Refactoring and cleanup. * Fixed broken spinner * Cosmetic improvements. Removed non-functional Upload context menu. Fixed error in response to move requests. Added minified version. * Added specific icons for text and image files. Fixed incompatibilities with SPIFFS. Fixed a race condition between deletion and reinsertion of nodes when multiple folders are refreshed. Fixed missing URL decoding for files with special chars (e.g. space char). Moved info from source code comment to a readme.md file. Added source PNG to git. Cleanup. * Added favicon.ico. * Renamed project * Small changes * Add a note about the ace.js dependency * Minor changes * Define LittleFS by default. If both uncompressed and gz versions exist, use uncompressed version. Small fixes. * Define LittleFS by default. If both uncompressed and gz versions exist, use uncompressed version. Small fixes. * Restyled version * (dummy edit to retrigger broken CI) * Using unsigned int for comparison with String.length() * Return an error when upload fails (e.g. filesystem full) * Trying to reorder functions to please CI * Reordered functions to please CI. * Update to use chunked response API * Removed temp files commited by mistake * Avoid using args() as requested * Use html entity for non-breaking space to avoid losing char when minifying * Script to preprocess index.htm * (reformated code) * (comments) * Preprocessed files * Fixed dump to create an actual include file * Optionally embed index.htm in code. (+ documentation and preprocessing script) * (reformated) * If editor cannot be loaded from the web, try a local version, or default to a text viewer if not present * (removed a TODO item :-)) * (forgot to reprocess files after last commit) * (reprocess should be ok this time) * Return error 500 when upload fails immediately (e.g. filesystem full) * Use standard <meter> tag for filesystem use * (updated following changes to index.htm) * Do not include gzipped version in the data folder by default. Leave it in the extras folder and change readme accordingly (plus some reformatingi of the readme file) * Gzipped index file not included in data/edit by default. It is now left in the extras folder. Readme file was updated accordingly (+ some reformating) * Reduce String clutter by reserving and concatenating elements one by one. * Use clear() to reset String. * Avoid comparisons against empty String. * Use char instead of single-char String where possible. * Prefer direct logic over inverted. * Rename returnBlah to replyBlah. * Renamed h2int to hexDigitToInt * Renamed getFileError() to checkForUnsupportedPath(), to avoid confusion with a getter. * Misc improvements. * Added comments about mandatory rebuilding gz and h files in case of update to index.htm. * Addressed a few comments. * Improve replies: bad requests vs server error * (reformated) * Reduce clutter by reserving String size beforehand. * Moved most Strings of more than 10 chars to flash. * Use lib version of urlDecode() instead of a local one, and only call it when required. * Added a comment about the dangers of recursion on embedded devices. * Added a more explicit warning in the .h header comment. * Added a typical set of required files to load ace editor from the ESP. * (reformated) * More explicit warning at the beginning of the .h version. Co-authored-by: david gauchard <gauchard@laas.fr> Co-authored-by: Earle F. Philhower, III <earlephilhower@yahoo.com> Co-authored-by: Develo <deveyes@gmail.com>
1129 lines
40 KiB
HTML
1129 lines
40 KiB
HTML
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
<!--
|
|
Important:
|
|
If you use the gzipped or `INCLUDE_FALLBACK_INDEX_HTM` options, please remember
|
|
to rerun the `reduce_index.sh` script located in the `extras` subfolder and recompile
|
|
the sketch after each change to this file.
|
|
-->
|
|
<html lang="en">
|
|
<head>
|
|
<title>File manager</title>
|
|
<style type="text/css" media="screen">
|
|
body {
|
|
font-size: 12px;
|
|
font-family: Verdana, Arial, Sans-serif;
|
|
}
|
|
.contextMenu {
|
|
z-index: 300;
|
|
position: absolute;
|
|
left: 5px;
|
|
border: 1px solid #444;
|
|
background-color: #F5F5F5;
|
|
display: none;
|
|
box-shadow: 0 0 10px rgba( 0, 0, 0, .4 );
|
|
font-size: 11px;
|
|
font-weight: bold;
|
|
}
|
|
.contextMenu ul {
|
|
list-style: none;
|
|
top: 0;
|
|
left: 0;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
.contextMenu li {
|
|
position: relative;
|
|
min-width: 60px;
|
|
cursor: pointer;
|
|
}
|
|
.contextMenu span {
|
|
color: #444;
|
|
display: inline-block;
|
|
padding: 6px;
|
|
}
|
|
.contextMenu li:hover { background: #444; }
|
|
.contextMenu li:hover span { color: #EEE; }
|
|
|
|
.css-treeview ul, .css-treeview li {
|
|
padding: 0;
|
|
margin: 0;
|
|
list-style: none;
|
|
}
|
|
.css-treeview input {
|
|
position: absolute;
|
|
opacity: 0;
|
|
}
|
|
.css-treeview {
|
|
font-size: 11px;
|
|
-moz-user-select: none;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
}
|
|
.css-treeview span {
|
|
color: #00f;
|
|
cursor: pointer;
|
|
}
|
|
.css-treeview span:hover {
|
|
text-decoration: underline;
|
|
}
|
|
.css-treeview input + label + ul {
|
|
margin: 0 0 0 22px;
|
|
}
|
|
.css-treeview input ~ ul {
|
|
display: none;
|
|
}
|
|
.css-treeview label, .css-treeview label::before {
|
|
cursor: pointer;
|
|
}
|
|
.css-treeview input:disabled + label {
|
|
cursor: default;
|
|
opacity: .6;
|
|
}
|
|
.css-treeview input:checked:not(:disabled) ~ ul {
|
|
display: block;
|
|
}
|
|
.css-treeview label, .css-treeview span, .css-treeview label::before {
|
|
background: url(" data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAACgCAMAAAAy5xwmAAAAb1BMVEVHcEyWm55WW2E6QUdAQEBiaG1jaG7///8vNj3KzM7CxMbm5ufMzc/Y2dqxtLaGio58gYWJjZFRVFZJTE/Lzc+vsbNdXV74+PhvdHlsbW2kp6q7vLzy8vMuNTzf39+ho6UyOUCFhYWUl5nv7+/p6ekQi/GRAAAAAXRSTlMAQObYZgAAASRJREFUSMftk9tywiAQhoUAhVAwJpqDRqfavv8zluNKkurEjrY3+e74JizLn2W1miAKxphOBMeUYq6uH/A3A66YpRBRqJZaMI8iwsV/CdM6A2xj5nIUsK0/iTUPrGNm0MUDIkeG/J54uOiksb9hXh55PhIIjfKgdJTHSEy2/FD06Xmod8NOwrrwFUsQmXZHZhhekHBiByYIoaIJAjP8USWiCA8KhEW8UGT7KPaZExsObHyrUgTkjTz8liPXwzwEB+OPFVxHE8X2ckRByLaVyhVKxP27KN+nAtH7Rvur2Dr621tekMes+WiG86Hr6jKYj+7Q1Kd0PpxI58NsQdN/25zlQDQdKb9AnM6fHSGkPsQaJamIpQzCrywhD1iT2XksLCws/IZv8Zof7t25YlkAAAAASUVORK5CYII=") no-repeat;
|
|
}
|
|
.css-treeview label, .css-treeview span, .css-treeview label::before, .css-treeview span::before {
|
|
display: inline-block;
|
|
height: 16px;
|
|
line-height: 16px;
|
|
vertical-align: middle;
|
|
}
|
|
.css-treeview label {
|
|
background-position: 18px 0; /* folder icon */
|
|
}
|
|
.css-treeview span {
|
|
background-position: 18px -48px; /* generic file icon */
|
|
}
|
|
.css-treeview span.txt {
|
|
background-position: 18px -64px; /* text file icon */
|
|
}
|
|
.css-treeview span.img {
|
|
background-position: 18px -80px; /* image file icon */
|
|
}
|
|
.css-treeview label::before {
|
|
content: "";
|
|
width: 16px;
|
|
margin: 0 22px 0 0;
|
|
vertical-align: middle;
|
|
background-position: 0 -32px; /* expand icon */
|
|
}
|
|
.css-treeview span::before {
|
|
content: "";
|
|
width: 16px;
|
|
margin: 0 22px 0 0;
|
|
}
|
|
.css-treeview input:checked + label::before {
|
|
background-position: 0 -16px; /* collapse icon */
|
|
}
|
|
|
|
/* webkit adjacent element selector bugfix */
|
|
@media screen and (-webkit-min-device-pixel-ratio:0)
|
|
{
|
|
.css-treeview{
|
|
-webkit-animation: webkit-adjacent-element-selector-bugfix infinite 1s;
|
|
}
|
|
|
|
@-webkit-keyframes webkit-adjacent-element-selector-bugfix
|
|
{
|
|
from {
|
|
padding: 0;
|
|
}
|
|
to {
|
|
padding: 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
#header {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
left: 0;
|
|
height:24px;
|
|
padding: 1px 1px 0px 10px;
|
|
background-color: #444;
|
|
color:#EEE;
|
|
}
|
|
#tree {
|
|
position: absolute;
|
|
top: 25px;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 20%;
|
|
padding: 8px;
|
|
}
|
|
#editor, #preview {
|
|
position: absolute;
|
|
top: 25px;
|
|
right: 0;
|
|
bottom: 0;
|
|
left: 20%;
|
|
}
|
|
#preview {
|
|
background-color: #EEE;
|
|
padding:5px;
|
|
}
|
|
|
|
#status {
|
|
position: absolute;
|
|
top: 3px;
|
|
right: 10px;
|
|
font-size: 15px;
|
|
}
|
|
|
|
#fsMeter {
|
|
width:100px;
|
|
padding-bottom:2px;
|
|
}
|
|
|
|
#warning {
|
|
height:100%;
|
|
background-color:orange;
|
|
color:black;
|
|
}
|
|
.tooltip {
|
|
position: absolute;
|
|
z-index: 2; /* must be above editor which is at 1 */
|
|
right:0px;
|
|
top:20px;
|
|
visibility: hidden;
|
|
background-color: white;
|
|
color: black;
|
|
text-align: center;
|
|
border: 1px solid #000;
|
|
padding: 3px;
|
|
font-size: 10px;
|
|
}
|
|
#warning:hover .tooltip {
|
|
visibility: visible;
|
|
}
|
|
|
|
#loading {
|
|
position:absolute;
|
|
// note: changing between block and none cancels the opacity anim, so we move it in and out of screen instead by changing the top value
|
|
// maybe see https://www.impressivewebs.com/animate-display-block-none/ for another approach
|
|
display:block;
|
|
top: -100vh;
|
|
left: 0;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
z-index: 100;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
opacity: 0;
|
|
transition: opacity 500ms ease-in-out;
|
|
}
|
|
#loading.shown {
|
|
top: 0;
|
|
opacity: 1;
|
|
}
|
|
|
|
#loading-msg {
|
|
display: inline-block;
|
|
position: absolute;
|
|
top: 0px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
color:white;
|
|
font-size: 32px;
|
|
}
|
|
|
|
@keyframes spinner-anim {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
/* :not(:required) hides this rule from IE9 and below */
|
|
.spinner-anim:not(:required) {
|
|
display: inline-block;
|
|
position: relative;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
animation: spinner-anim 1s infinite linear;
|
|
border: 16px solid #eee;
|
|
border-right-color: transparent;
|
|
border-radius: 32px;
|
|
box-sizing: border-box;
|
|
overflow: hidden;
|
|
text-indent: -9999px;
|
|
width: 64px;
|
|
height: 64px;
|
|
}
|
|
</style>
|
|
<script>
|
|
var tree; // needed by editor to refresh file size on save
|
|
var fsInfo; // needed to change refreshPath behaviour on SPIFFS
|
|
/////////////////////////
|
|
// UTILS
|
|
|
|
function setLoading(show, message) {
|
|
if (message) console.log(message);
|
|
document.getElementById("loading-msg").innerHTML = message?message:"";
|
|
if (show) document.getElementById("loading").classList.add("shown");
|
|
else document.getElementById("loading").classList.remove("shown");
|
|
document.body.style.cursor = show?"wait":"default";
|
|
}
|
|
|
|
function readableSize(bytes) {
|
|
if (bytes < 1024) return bytes + " B";
|
|
var units = [' KiB', ' MiB', ' GiB', ' TiB', 'PiB'];
|
|
var i = -1;
|
|
do {
|
|
bytes = bytes / 1024;
|
|
i++;
|
|
} while (bytes > 1024);
|
|
return bytes.toFixed(2) + units[i];
|
|
}
|
|
|
|
function refreshStatus(){
|
|
document.getElementById("status").innerHTML = "(refreshing...)";
|
|
var xmlHttp = new XMLHttpRequest();
|
|
xmlHttp.onload = function() {
|
|
if (xmlHttp.status != 200) showHttpError(xmlHttp);
|
|
else {
|
|
fsInfo = JSON.parse(xmlHttp.responseText);
|
|
var status = fsInfo.type + " - ";
|
|
if (fsInfo.isOk) {
|
|
var percent = (1+Math.round(99*fsInfo.usedBytes/fsInfo.totalBytes)); // fake to see the "used" bar in any case
|
|
var text = readableSize(fsInfo.totalBytes - fsInfo.usedBytes) + " free of " + readableSize(fsInfo.totalBytes);
|
|
status += "<meter id='fsMeter' min='0' optimum='0'low='90' high='95' max='100' value='" + percent + "' title='" + text + "'>" + text + "</meter>"
|
|
if (fsInfo.unsupportedFiles) {
|
|
status += " <span id='warning'>WARNING<span class='tooltip'>"
|
|
+ "Filesystem contains unsupported filenames:<br/>"
|
|
+ fsInfo.unsupportedFiles
|
|
+ "</span></span>";
|
|
}
|
|
}
|
|
else {
|
|
status += "<span style='background-color:red;color:white;font-weight:bold'>INIT ERROR !</span>";
|
|
}
|
|
document.getElementById("status").innerHTML = status;
|
|
if (fsInfo.type != "SPIFFS") {
|
|
document.getElementById("mkdir").style.display = "inline";
|
|
}
|
|
}
|
|
};
|
|
xmlHttp.open("GET", "/status", true);
|
|
xmlHttp.send(null);
|
|
}
|
|
|
|
function showHttpError(request) {
|
|
alert("ERROR: [" + request.status+"] " + request.responseText);
|
|
}
|
|
|
|
function canLoadNewContents() {
|
|
// The fact the save button is enabled indicates the editor has unsaved changes
|
|
if (document.getElementById("saveBtn").disabled) return true;
|
|
if (confirm("Changes to your document will be lost if you continue")) {
|
|
enableSaveDiscardBtns(false);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function enableSaveDiscardBtns(enabled) {
|
|
document.getElementById("saveBtn").disabled = !enabled;
|
|
document.getElementById("discardBtn").disabled = !enabled;
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns the parent folder of a given path
|
|
* "" > ""
|
|
* "/" > ""
|
|
* "a" > ""
|
|
* "/a" > ""
|
|
* "a/" > ""
|
|
* "/a/" > ""
|
|
* "a/b" > "/a"
|
|
* "/a/b" > "/a"
|
|
* "a/b/" > "/a"
|
|
* "/a/b/" > "/a"
|
|
*/
|
|
function getParentFolder(path) {
|
|
if (!path.startsWith("/")) path = "/" + path;
|
|
if (path.endsWith("/")) path = path.slice(0, -1);
|
|
return path.substring(0, path.lastIndexOf("/"));
|
|
}
|
|
|
|
|
|
/////////////////////////
|
|
// HEADER with uploader, buttons ans status
|
|
|
|
function createHeader(element, tree, editor){
|
|
var header = document.getElementById(element);
|
|
var xmlHttp;
|
|
var fileSelector = document.createElement("input");
|
|
fileSelector.type = "file";
|
|
fileSelector.multiple = false;
|
|
fileSelector.name = "data";
|
|
header.appendChild(fileSelector);
|
|
|
|
var pathInput = document.createElement("input");
|
|
pathInput.id = "pathInput";
|
|
pathInput.type = "text";
|
|
pathInput.name = "path";
|
|
pathInput.defaultValue = "/";
|
|
header.appendChild(pathInput);
|
|
|
|
var upload = document.createElement("button");
|
|
upload.innerHTML = "Upload";
|
|
header.appendChild(upload);
|
|
|
|
var mkdir = document.createElement("button");
|
|
mkdir.id = "mkdir";
|
|
mkdir.style.display = "none";
|
|
mkdir.innerHTML = "MkDir";
|
|
header.appendChild(mkdir);
|
|
|
|
var mkfile = document.createElement("button");
|
|
mkfile.innerHTML = "MkFile";
|
|
header.appendChild(mkfile);
|
|
|
|
var editorButtons = document.createElement("span");
|
|
editorButtons.id = "editorButtons";
|
|
editorButtons.style.display = "none";
|
|
header.appendChild(editorButtons);
|
|
|
|
var save = document.createElement("button");
|
|
save.id = "saveBtn";
|
|
save.innerHTML = "Save";
|
|
save.style.marginLeft = "30px";
|
|
save.disabled = true;
|
|
editorButtons.appendChild(save);
|
|
|
|
var discard = document.createElement("button");
|
|
discard.id = "discardBtn";
|
|
discard.innerHTML = "Discard";
|
|
discard.disabled = true;
|
|
editorButtons.appendChild(discard);
|
|
|
|
var help = document.createElement("button");
|
|
help.id = "helpBtn";
|
|
help.innerHTML = "?";
|
|
editorButtons.appendChild(help);
|
|
|
|
var status = document.createElement("span");
|
|
status.id = "status";
|
|
status.innerHTML = "(loading)";
|
|
header.appendChild(status);
|
|
|
|
fileSelector.onchange = function(e){ // A file has been selected
|
|
if (fileSelector.files.length === 0) return;
|
|
var filename = fileSelector.files[0].name;
|
|
if (mustFormat83()) {
|
|
filename = to83(filename);
|
|
}
|
|
if (!pathInput.value.startsWith("/")) pathInput.value = "/" + pathInput.value;
|
|
pathInput.value = getParentFolder(pathInput.value) + "/" + filename;
|
|
}
|
|
|
|
upload.onclick = function(e) {
|
|
if (fileSelector.files.length === 0) return;
|
|
tree.httpUpload(fileSelector.files[0], pathInput.value);
|
|
}
|
|
|
|
function mustFormat83() {
|
|
return false; // TODO: if needed, test fsInfo.type (and fatType ?)
|
|
}
|
|
|
|
function to83(filename) {
|
|
var ext = /(?:\.([^.]+))?$/.exec(filename)[1];
|
|
var name = /(.*)\.[^.]+$/.exec(filename)[1];
|
|
if (name !== undefined){
|
|
if (name.length > 8) name = name.substring(0, 8);
|
|
filename = name;
|
|
}
|
|
if (ext !== undefined){
|
|
if (ext === "html") ext = "htm";
|
|
else if (ext === "jpeg") ext = "jpg";
|
|
filename = filename + "." + ext;
|
|
}
|
|
return filename;
|
|
}
|
|
|
|
mkfile.onclick = function(e){
|
|
var path = pathInput.value.trim();
|
|
if (path === "") alert("A filename must be given");
|
|
else if (path.endsWith("/")) alert("Filenames must not end with a '/' character");
|
|
else tree.httpCreate(path);
|
|
};
|
|
|
|
mkdir.onclick = function(e){
|
|
var path = pathInput.value.trim();
|
|
if (path === "") alert("A folder name must be given");
|
|
else {
|
|
if (!path.endsWith("/")) path = path + "/";
|
|
tree.httpCreate(path);
|
|
}
|
|
};
|
|
|
|
save.onclick = function(e){
|
|
editor.save();
|
|
}
|
|
|
|
discard.onclick = function(e){
|
|
editor.discard();
|
|
}
|
|
|
|
help.onclick = function(e){
|
|
editor.showShortcuts();
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/////////////////////////
|
|
// FILE TREE
|
|
|
|
function createTree(element, editor){
|
|
var preview = document.getElementById("preview");
|
|
var treeRoot = document.createElement("div");
|
|
treeRoot.className = "css-treeview";
|
|
treeRoot.id = "/";
|
|
document.getElementById(element).appendChild(treeRoot);
|
|
|
|
function loadDownload(path){
|
|
document.getElementById("download-frame").src = path+"?download=true";
|
|
}
|
|
|
|
function loadImgPreview(path){
|
|
document.getElementById("pathInput").value = path;
|
|
document.getElementById("editor").style.display = "none";
|
|
document.getElementById("editorButtons").style.display = "none";
|
|
preview.style.display = "block";
|
|
preview.innerHTML = "<img src='"+path+"' style='max-width:100%; max-height:100%; margin:auto; display:block;' />";
|
|
}
|
|
|
|
// Fall back code used when ace.js is not available (neither online or local)
|
|
function loadTxtPreview(path){
|
|
document.getElementById("pathInput").value = path;
|
|
document.getElementById("editor").style.display = "none";
|
|
document.getElementById("editorButtons").style.display = "none";
|
|
preview.style.display = "block";
|
|
preview.innerHTML = "<span style='color:red;'>Ace editor could not be loaded from the internet nor from /edit/ace.js . Defaulting to text viewer...</span><pre id='txtContents' style='overflow: auto;'></pre>";
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.onload = function () {
|
|
document.getElementById('txtContents').textContent = this.responseText;
|
|
};
|
|
xhr.open('GET', path);
|
|
xhr.send();
|
|
}
|
|
|
|
this.clearMainPanel = function() {
|
|
document.getElementById("editor").style.display = "none";
|
|
document.getElementById("editorButtons").style.display = "none";
|
|
preview.style.display = "block";
|
|
preview.innerHTML = "<div style='text-align:center'>(file not found or format not supported)</div>";
|
|
}
|
|
|
|
function isTextFile(path){
|
|
var ext = /(?:\.([^.]+))?$/.exec(path)[1];
|
|
if (ext !== undefined){
|
|
switch(ext.toLowerCase()){
|
|
case "txt":
|
|
case "htm":
|
|
case "html":
|
|
case "js":
|
|
case "json":
|
|
case "c":
|
|
case "h":
|
|
case "cpp":
|
|
case "css":
|
|
case "xml":
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isImageFile(path){
|
|
var ext = /(?:\.([^.]+))?$/.exec(path)[1];
|
|
if (ext !== undefined){
|
|
switch(ext.toLowerCase()){
|
|
case "png":
|
|
case "jpg":
|
|
case "jpeg":
|
|
case "gif":
|
|
case "ico":
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function fillFolderMenu(el, path){
|
|
var list = document.createElement("ul");
|
|
el.appendChild(list);
|
|
var action = document.createElement("li");
|
|
list.appendChild(action);
|
|
var isChecked = document.getElementById(path).childNodes[0].checked;
|
|
var expnd = document.createElement("li");
|
|
list.appendChild(expnd);
|
|
if (isChecked){
|
|
expnd.innerHTML = "<span>Collapse</span>";
|
|
expnd.onclick = function(e){
|
|
if (document.body.getElementsByClassName('contextMenu').length > 0) document.body.removeChild(el);
|
|
document.getElementById(path).childNodes[0].checked = false;
|
|
};
|
|
var refrsh = document.createElement("li");
|
|
list.appendChild(refrsh);
|
|
refrsh.innerHTML = "<span>Refresh</span>";
|
|
refrsh.onclick = function(e){
|
|
if (document.body.getElementsByClassName('contextMenu').length > 0) document.body.removeChild(el);
|
|
httpList(path);
|
|
};
|
|
}
|
|
else {
|
|
expnd.innerHTML = "<span>Expand</span>";
|
|
expnd.onclick = function(e){
|
|
if (document.body.getElementsByClassName('contextMenu').length > 0) document.body.removeChild(el);
|
|
document.getElementById(path).childNodes[0].checked = true;
|
|
httpList(path);
|
|
};
|
|
}
|
|
var renFolder = document.createElement("li");
|
|
list.appendChild(renFolder);
|
|
renFolder.innerHTML = "<span>Rename/Move</span>";
|
|
renFolder.onclick = function(e){
|
|
if (document.body.getElementsByClassName('contextMenu').length > 0) document.body.removeChild(el);
|
|
var newPath = prompt("Rename " + path + " to", path);
|
|
if (newPath != null && newPath != path) {
|
|
httpRename(path, newPath);
|
|
}
|
|
};
|
|
var delFolder = document.createElement("li");
|
|
list.appendChild(delFolder);
|
|
delFolder.innerHTML = "<span>Delete</span>";
|
|
delFolder.onclick = function(e){
|
|
if (document.body.getElementsByClassName('contextMenu').length > 0) document.body.removeChild(el);
|
|
httpDelete(path);
|
|
};
|
|
}
|
|
|
|
function fillFileMenu(el, path){
|
|
var list = document.createElement("ul");
|
|
el.appendChild(list);
|
|
var action = document.createElement("li");
|
|
list.appendChild(action);
|
|
if (isTextFile(path)){
|
|
if (typeof ace == "undefined") {
|
|
// Could not load editor
|
|
action.innerHTML = "<span>View</span>";
|
|
action.onclick = function(e){
|
|
if (document.body.getElementsByClassName('contextMenu').length > 0) document.body.removeChild(el);
|
|
if (canLoadNewContents()) loadTxtPreview(path);
|
|
};
|
|
}
|
|
else {
|
|
action.innerHTML = "<span>Edit</span>";
|
|
action.onclick = function(e){
|
|
if (document.body.getElementsByClassName('contextMenu').length > 0) document.body.removeChild(el);
|
|
if (canLoadNewContents()) editor.loadUrl(path);
|
|
};
|
|
}
|
|
}
|
|
else if (isImageFile(path)){
|
|
action.innerHTML = "<span>Preview</span>";
|
|
action.onclick = function(e){
|
|
if (document.body.getElementsByClassName('contextMenu').length > 0) document.body.removeChild(el);
|
|
if (canLoadNewContents()) loadImgPreview(path);
|
|
};
|
|
}
|
|
var download = document.createElement("li");
|
|
list.appendChild(download);
|
|
download.innerHTML = "<span>Download</span>";
|
|
download.onclick = function(e){
|
|
if (document.body.getElementsByClassName('contextMenu').length > 0) document.body.removeChild(el);
|
|
loadDownload(path);
|
|
};
|
|
var renFile = document.createElement("li");
|
|
list.appendChild(renFile);
|
|
renFile.innerHTML = "<span>Rename/Move</span>";
|
|
renFile.onclick = function(e){
|
|
if (document.body.getElementsByClassName('contextMenu').length > 0) document.body.removeChild(el);
|
|
var newPath = prompt("Rename " + path + " to", path);
|
|
if (newPath != null && newPath != path) {
|
|
httpRename(path, newPath);
|
|
}
|
|
};
|
|
var delFile = document.createElement("li");
|
|
list.appendChild(delFile);
|
|
delFile.innerHTML = "<span>Delete</span>";
|
|
delFile.onclick = function(e){
|
|
if (document.body.getElementsByClassName('contextMenu').length > 0) document.body.removeChild(el);
|
|
httpDelete(path);
|
|
};
|
|
}
|
|
|
|
function showContextMenu(e, path, isfile){
|
|
var divContext = document.createElement("div");
|
|
var scrollTop = document.body.scrollTop ? document.body.scrollTop : document.documentElement.scrollTop;
|
|
var scrollLeft = document.body.scrollLeft ? document.body.scrollLeft : document.documentElement.scrollLeft;
|
|
var left = e.clientX + scrollLeft;
|
|
var top = e.clientY + scrollTop;
|
|
divContext.className = 'contextMenu';
|
|
divContext.style.display = 'block';
|
|
divContext.style.left = left + 'px';
|
|
divContext.style.top = top + 'px';
|
|
if (isfile) fillFileMenu(divContext, path);
|
|
else fillFolderMenu(divContext, path);
|
|
document.body.appendChild(divContext);
|
|
var width = divContext.offsetWidth;
|
|
var height = divContext.offsetHeight;
|
|
divContext.onmouseout = function(e){
|
|
if (e.clientX < left || e.clientX > (left + width) || e.clientY < top || e.clientY > (top + height)){
|
|
if (document.body.getElementsByClassName('contextMenu').length > 0) document.body.removeChild(divContext);
|
|
}
|
|
};
|
|
}
|
|
|
|
function createTreeLeaf(parentPath, name, size){
|
|
var leaf = document.createElement("li");
|
|
leaf.id = ((parentPath == "/")?"":parentPath)+"/"+name;
|
|
var span = document.createElement("span");
|
|
if (isTextFile(name)) span.classList.add("txt");
|
|
else if (isImageFile(name)) span.classList.add("img");
|
|
span.innerHTML = name + " <i>(" + readableSize(size) + ")</i>";
|
|
leaf.appendChild(span);
|
|
leaf.onclick = function(e){
|
|
attemptLoad(leaf.id);
|
|
};
|
|
leaf.oncontextmenu = function(e){
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
showContextMenu(e, leaf.id, true);
|
|
};
|
|
return leaf;
|
|
}
|
|
|
|
function createTreeBranch(parentPath, name, disabled){
|
|
var leaf = document.createElement("li");
|
|
leaf.id = ((parentPath == "/")?"":parentPath)+"/"+name;
|
|
var check = document.createElement("input");
|
|
check.type = "checkbox";
|
|
if (typeof disabled !== "undefined" && disabled) check.disabled = "disabled";
|
|
leaf.appendChild(check);
|
|
var label = document.createElement("label");
|
|
label.textContent = name;
|
|
leaf.appendChild(label);
|
|
check.onchange = function(e){
|
|
if (check.checked){
|
|
httpList(leaf.id);
|
|
}
|
|
};
|
|
label.onclick = function(e){
|
|
if (!check.checked){
|
|
check.checked = true;
|
|
httpList(leaf.id);
|
|
}
|
|
else {
|
|
check.checked = false;
|
|
}
|
|
};
|
|
leaf.oncontextmenu = function(e){
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
showContextMenu(e, leaf.id, false);
|
|
}
|
|
return leaf;
|
|
}
|
|
|
|
function addFileNodes(parentPath, items){
|
|
items.sort(function(a, b) {
|
|
if (a.type == b.type) {
|
|
return a.name.localeCompare(b.name); // a before z
|
|
}
|
|
else {
|
|
return a.type.localeCompare(b.type); // dir before file
|
|
}
|
|
});
|
|
var list = document.createElement("ul");
|
|
|
|
document.getElementById(parentPath).appendChild(list);
|
|
var ll = items.length;
|
|
for(var i = 0; i < ll; i++){
|
|
var item = items[i];
|
|
var itemEl;
|
|
if (item.type === "file") itemEl = createTreeLeaf(parentPath, item.name, item.size);
|
|
else itemEl = createTreeBranch(parentPath, item.name);
|
|
list.appendChild(itemEl);
|
|
}
|
|
}
|
|
|
|
this.attemptLoad = function(path) {
|
|
console.log("Attempting load of '" + path + "'...");
|
|
document.getElementById("pathInput").value = path;
|
|
if (canLoadNewContents()) {
|
|
if (isTextFile(path)) {
|
|
if (typeof ace == "undefined") {
|
|
loadTxtPreview(path);
|
|
}
|
|
else {
|
|
editor.loadUrl(path);
|
|
}
|
|
}
|
|
else if (isImageFile(path)) loadImgPreview(path);
|
|
else clearMainPanel();
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Refresh the given path, e.g. "/a/b/c/d"
|
|
* It means we have to make sure d is already displayed
|
|
* If not, we get to its parent first (c), if open, and so on (b, a).
|
|
* Once we have found an ancestor, we refresh it, then we must come back to where we started, refreshing nodes on our way down.
|
|
* This is done by pushing all paths to be refreshed in an array that will be pop'd in the callback function
|
|
*/
|
|
this.refreshPath = function(path){
|
|
if (fsInfo.type == "SPIFFS") {
|
|
// on SPIFFS : No parent node => refresh full tree
|
|
console.log("Refreshing '/'...");
|
|
httpList("/");
|
|
}
|
|
else {
|
|
console.log("Refreshing '" + path + "'...");
|
|
if (path.lastIndexOf("/") == -1){
|
|
// No "/" => reset the root
|
|
httpList("/");
|
|
}
|
|
else {
|
|
var paths = [];
|
|
// Climb the tree until we get to an open node, adding paths to the array
|
|
var parentPath = path;
|
|
while ((parentPath.lastIndexOf("/") != -1) && !document.getElementById(parentPath)) {
|
|
paths.push(parentPath);
|
|
parentPath = getParentFolder(parentPath);
|
|
}
|
|
|
|
// If we've reached the top
|
|
if (parentPath == "") paths.push("/"); // list the root
|
|
else paths.push(parentPath); // otherwise list the last folder
|
|
|
|
// And list it back down by iterating on collected paths
|
|
listPaths(paths);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//////////////////////////////
|
|
// HTTP OPERATIONS
|
|
|
|
// Callbacks
|
|
|
|
function onListReceived(req, parentPath, remainingPaths) {
|
|
return function(){
|
|
setLoading(false);
|
|
if (req.status != 200) showHttpError(req);
|
|
else {
|
|
// Remove previous child list, if any
|
|
var parentEl = document.getElementById(parentPath);
|
|
if (parentEl) {
|
|
var lastChild = parentEl.lastElementChild;
|
|
if (lastChild && lastChild.tagName == "UL") parentEl.removeChild(lastChild);
|
|
}
|
|
// And reinsert the received ones instead
|
|
addFileNodes(parentPath, JSON.parse(req.responseText));
|
|
if (document.getElementById(parentPath).childNodes[0].checked !== undefined) {
|
|
document.getElementById(parentPath).childNodes[0].checked = true;
|
|
}
|
|
// If there are more folders to refresh, go ahead
|
|
if (remainingPaths && remainingPaths.length) {
|
|
listPaths(remainingPaths);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Callback after a FS operation was performed.
|
|
* The "req" param is the http request
|
|
* The "path" param is the "affected" (created, moved, renamed, delteted) path, of which the parent must be refreshed
|
|
* In case of move/delete, the "target" path is returned in the response, so that it can be refreshed too
|
|
* Operation | path (file to be loaded) | req.responseText (between brackets if same as parent of path)
|
|
* ---------------+--------------------------+--------------------------------------------------------------
|
|
* Create file | created file | (parent of created file)
|
|
* Create folder | created folder | (parent of created folder)
|
|
* Rename file | target file | (parent of source file)
|
|
* Move file | target file | parent of source file, or remaining ancestor
|
|
? Rename folder | target folder | (parent of source folder)
|
|
? Move folder | target folder | parent of source folder, or remaining ancestor
|
|
* Delete file | | parent of deleted file, or remaining ancestor
|
|
* Delete folder | | parent of deleted folder, or remaining ancestor
|
|
*/
|
|
function onOperationComplete(req, path){
|
|
return function(){
|
|
setLoading(false);
|
|
if (req.status != 200) showHttpError(req);
|
|
else {
|
|
if (path) {
|
|
var parentPath = getParentFolder(path);
|
|
|
|
// Refresh returned path, if requested and different from path
|
|
if (req.responseText && req.responseText != parentPath) {
|
|
refreshPath(req.responseText);
|
|
}
|
|
|
|
// Refresh original path
|
|
refreshPath(parentPath);
|
|
|
|
// Try to load given path
|
|
attemptLoad(path);
|
|
}
|
|
else {
|
|
// Delete, only refresh returned path
|
|
refreshPath(req.responseText);
|
|
}
|
|
}
|
|
refreshStatus();
|
|
}
|
|
}
|
|
|
|
|
|
// Requests
|
|
|
|
function httpList(parentPath, remainingPaths){
|
|
setLoading(true, "Listing '" + parentPath + "'...");
|
|
// Fetch an updated list
|
|
xmlHttp = new XMLHttpRequest();
|
|
xmlHttp.onload = onListReceived(xmlHttp, parentPath, remainingPaths);
|
|
xmlHttp.open("GET", "/list?dir="+parentPath, true);
|
|
xmlHttp.send(null);
|
|
}
|
|
|
|
function listPaths(paths) {
|
|
var path = paths.pop();
|
|
if (path) {
|
|
httpList(path, paths);
|
|
}
|
|
}
|
|
|
|
|
|
this.httpUpload = function(file, path) {
|
|
setLoading(true, "Uploading '" + path + "'...");
|
|
if (!path.startsWith("/")) path = "/" + path;
|
|
xmlHttp = new XMLHttpRequest();
|
|
xmlHttp.onload = onOperationComplete(xmlHttp, path);
|
|
var formData = new FormData();
|
|
formData.append("data", file, path);
|
|
xmlHttp.open("POST", "/edit");
|
|
xmlHttp.send(formData);
|
|
};
|
|
|
|
this.httpCreate = function(path){
|
|
setLoading(true, "Creating '" + path + "'...");
|
|
if (!path.startsWith("/")) path = "/" + path;
|
|
xmlHttp = new XMLHttpRequest();
|
|
xmlHttp.onload = onOperationComplete(xmlHttp, path);
|
|
var formData = new FormData();
|
|
formData.append("path", path);
|
|
xmlHttp.open("PUT", "/edit");
|
|
xmlHttp.send(formData);
|
|
}
|
|
|
|
function httpRename(srcPath, dstPath){
|
|
setLoading(true, "Renaming '" + srcPath + "' to '" + dstPath + "'...");
|
|
if (!dstPath.startsWith("/")) dstPath = "/" + dstPath;
|
|
xmlHttp = new XMLHttpRequest();
|
|
xmlHttp.onload = onOperationComplete(xmlHttp, dstPath);
|
|
var formData = new FormData();
|
|
formData.append("path", dstPath);
|
|
formData.append("src", srcPath);
|
|
xmlHttp.open("PUT", "/edit");
|
|
xmlHttp.send(formData);
|
|
}
|
|
|
|
function httpDelete(path) {
|
|
setLoading(true, "Deleting '" + path + "'...");
|
|
xmlHttp = new XMLHttpRequest();
|
|
xmlHttp.onload = onOperationComplete(xmlHttp);
|
|
var formData = new FormData();
|
|
formData.append("path", path);
|
|
xmlHttp.open("DELETE", "/edit");
|
|
xmlHttp.send(formData);
|
|
}
|
|
|
|
|
|
httpList("/");
|
|
return this;
|
|
}
|
|
|
|
/////////////////////////
|
|
// ACE EDITOR MANAGEMENT
|
|
|
|
function createEditor(element, file, lang, theme, type){
|
|
function getLangFromFilename(filename){
|
|
var lang = "plain";
|
|
var ext = /(?:\.([^.]+))?$/.exec(filename)[1];
|
|
if (ext !== undefined){
|
|
switch(ext){
|
|
case "txt": lang = "plain"; break;
|
|
case "htm": lang = "html"; break;
|
|
case "js": lang = "javascript"; break;
|
|
case "c": lang = "c_cpp"; break;
|
|
case "cpp": lang = "c_cpp"; break;
|
|
case "css":
|
|
case "scss":
|
|
case "php":
|
|
case "html":
|
|
case "json":
|
|
case "xml":
|
|
lang = ext;
|
|
}
|
|
}
|
|
return lang;
|
|
}
|
|
|
|
if (typeof file === "undefined") file = "/index.htm";
|
|
|
|
if (typeof lang === "undefined"){
|
|
lang = getLangFromFilename(file);
|
|
}
|
|
|
|
if (typeof theme === "undefined") theme = "textmate";
|
|
|
|
if (typeof type === "undefined"){
|
|
type = "text/"+lang;
|
|
if (lang === "c_cpp") type = "text/plain";
|
|
}
|
|
|
|
var xmlHttp = null;
|
|
var editor = ace.edit(element);
|
|
|
|
function filePosted(){
|
|
setLoading(false);
|
|
if (xmlHttp.status != 200) showHttpError(xmlHttp);
|
|
tree.refreshPath(getParentFolder(file)); // to update size in tree
|
|
refreshStatus();
|
|
}
|
|
function postFile(path, data, type){
|
|
setLoading(true, "Saving '" + path + "'...");
|
|
xmlHttp = new XMLHttpRequest();
|
|
xmlHttp.onload = filePosted;
|
|
var formData = new FormData();
|
|
formData.append("data", new Blob([data], { type: type }), path);
|
|
xmlHttp.open("POST", "/edit");
|
|
xmlHttp.send(formData);
|
|
}
|
|
|
|
function fileLoaded(){
|
|
setLoading(false);
|
|
document.getElementById("preview").style.display = "none";
|
|
document.getElementById("editor").style.display = "block";
|
|
document.getElementById("editorButtons").style.display = "inline";
|
|
if (xmlHttp.status == 200) {
|
|
editor.setValue(xmlHttp.responseText);
|
|
editor.clearSelection();
|
|
enableSaveDiscardBtns(false);
|
|
}
|
|
else tree.clearMainPanel();
|
|
}
|
|
function loadFile(path){
|
|
setLoading(true, "Loading '" + path + "'...");
|
|
xmlHttp = new XMLHttpRequest();
|
|
xmlHttp.onload = fileLoaded;
|
|
xmlHttp.open("GET", path, true);
|
|
xmlHttp.send(null);
|
|
}
|
|
|
|
if (lang !== "plain") editor.getSession().setMode("ace/mode/"+lang);
|
|
editor.setTheme("ace/theme/"+theme);
|
|
editor.$blockScrolling = Infinity;
|
|
editor.getSession().setUseSoftTabs(true);
|
|
editor.getSession().setTabSize(2);
|
|
editor.setHighlightActiveLine(true);
|
|
editor.setShowPrintMargin(false);
|
|
editor.commands.addCommand({
|
|
name: 'save',
|
|
bindKey: {win: 'Ctrl-S', mac: 'Command-S'},
|
|
exec: function(editor) {
|
|
editor.save();
|
|
},
|
|
readOnly: false
|
|
});
|
|
editor.commands.addCommand({
|
|
name: "showKeyboardShortcuts",
|
|
bindKey: {win: "Ctrl-Alt-h", mac: "Command-Alt-h"},
|
|
exec: function(editor) {
|
|
editor.showShortcuts();
|
|
}
|
|
})
|
|
|
|
editor.session.on('change', function(delta) {
|
|
enableSaveDiscardBtns(true);
|
|
});
|
|
|
|
editor.loadUrl = function(path){
|
|
document.getElementById("pathInput").value = path;
|
|
enableSaveDiscardBtns(false);
|
|
file = path;
|
|
lang = getLangFromFilename(file);
|
|
type = "text/"+lang;
|
|
if (lang !== "plain") editor.getSession().setMode("ace/mode/"+lang);
|
|
loadFile(file);
|
|
}
|
|
|
|
editor.save = function() {
|
|
enableSaveDiscardBtns(false);
|
|
postFile(file, editor.getValue()+"", type);
|
|
}
|
|
|
|
editor.discard = function() {
|
|
editor.loadUrl(file);
|
|
enableSaveDiscardBtns(false);
|
|
}
|
|
|
|
editor.showShortcuts = function() {
|
|
ace.config.loadModule("ace/ext/keybinding_menu", function(module) {
|
|
module.init(editor);
|
|
editor.showKeyboardShortcuts()
|
|
});
|
|
}
|
|
|
|
loadFile(file);
|
|
|
|
return editor;
|
|
}
|
|
|
|
/////////////////////////
|
|
// MAIN ENTRY POINT
|
|
|
|
function onBodyLoad(){
|
|
var vars = {};
|
|
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) { vars[key] = value; });
|
|
if (typeof ace != "undefined") {
|
|
var editor = createEditor("editor", vars.file, vars.lang, vars.theme);
|
|
}
|
|
tree = createTree("tree", editor);
|
|
createHeader("header", tree, editor);
|
|
refreshStatus();
|
|
};
|
|
</script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.9/ace.js" type="text/javascript" charset="utf-8"></script>
|
|
<script>
|
|
if (typeof ace == "undefined") {
|
|
console.log("Cannot load ace.js from the web, trying local copy");
|
|
var script = document.createElement('script');
|
|
script.src = "/edit/ace.js";
|
|
script.async = false;
|
|
document.head.appendChild(script);
|
|
}
|
|
</script>
|
|
</head>
|
|
<body onload="onBodyLoad();">
|
|
<div id="header"></div>
|
|
<div id="tree"></div>
|
|
<div id="editor"></div>
|
|
<div id="preview" style="display:none;"></div>
|
|
<div id="loading"><span id="loading-msg"></span><br/><div class="spinner-anim">Loading...</div></div>
|
|
<iframe id=download-frame style='display:none;'></iframe>
|
|
</body>
|
|
</html>
|