Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SpiffsEditor #102

Closed
eketjall opened this issue Nov 29, 2016 · 7 comments
Closed

SpiffsEditor #102

eketjall opened this issue Nov 29, 2016 · 7 comments
Labels

Comments

@eketjall
Copy link

I'm probably just stupid and missing something, but is the editor actually supposed to be able to edit files?
I get the file listing and kan view file content.
I can also upload files.
But how can I edit existing files? My first thought is that a save-button is missing.
I can also create new files, but the content of the file is always just a singel dot

spiffseditor
.

@me-no-dev
Copy link
Owner

yeah... i keep forgetting to add save button, but Ctrl+S on Windows/Linux and Cmd+S on Mac do the job of saving

@eketjall
Copy link
Author

So for creating a new file the steps is the following?
1 - create file
2 - edit file
3 - save file with Ctrl+S

@me-no-dev
Copy link
Owner

yes, or open->edit->ctrl+s

@hallard
Copy link
Contributor

hallard commented Jan 16, 2017

Here is the modified index.htm file extracted, with monokai theme and save button :-)
I'm always using file directly from SPIFFS system because when testing it's really easy doing changes (with the editor itself) and test, and almost if you use SPIFFSEditor, mainly you have SPIFFS system file to put file into. That's just a matter of preference of course, but If you want to update you need to gz file then create the C code to put into SPIFFSEditor.cpp

To use directly edit.htm file I just changed info SPIFFSEditor.cpp lines
by

  //AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", edit_htm_gz, edit_htm_gz_len);
  //response->addHeader("Content-Encoding", "gzip");
  //request->send(response);
  //Send edit.htm with default content type
  request->send(SPIFFS, "/edit.htm");

and of course removed edit_htm_gz

Editor looks like
image

Modified file index.htm

<!-- This file has been written by Me-No-Dev
see https://github.com/me-no-dev/ and is part
of the sample code of ESPAsyncWebServer
see https://github.com/me-no-dev/ESPAsyncWebServer -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>ESP8266 SPIFFS File Editor</title>
<meta name="author" content="Me-No-Dev" />
<meta name="Description" content="ESP8266 SPIFFS File Web Editor"/>
<link rel="shortcut icon" href="favicon.ico"/>
<style type="text/css" media="screen">
.cm {
  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: 12px;
  font-family: sans-serif;
  font-weight:bold;
}
.cm ul {
  list-style: none;
  top: 0;
  left: 0;
  margin: 0;
  padding: 0;
}
.cm li {
  position: relative;
  min-width: 60px;
  cursor: pointer;
}
.cm span {
  color: #444;
  display: inline-block;
  padding: 6px;
}
.cm li:hover { background: #444; }
.cm li:hover span { color: #EEE; }
.tvu ul, .tvu li {
  padding: 0;
  margin: 0;
  list-style: none;
}
.tvu input {
  position: absolute;
  opacity: 0;
}
.tvu {
  font: normal 12px Verdana, Arial, Sans-serif;
  -moz-user-select: none;
  -webkit-user-select: none;
  user-select: none;
  color: #444;
  line-height: 16px;
}
.tvu span {
  margin-bottom:5px;
  padding: 0 0 0 18px;
  cursor: pointer;
  display: inline-block;
  height: 16px;
  vertical-align: middle;
  background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAADoSURBVBgZBcExblNBGAbA2ceegTRBuIKOgiihSZNTcC5LUHAihNJR0kGKCDcYJY6D3/77MdOinTvzAgCw8ysThIvn/VojIyMjIyPP+bS1sUQIV2s95pBDDvmbP/mdkft83tpYguZq5Jh/OeaYh+yzy8hTHvNlaxNNczm+la9OTlar1UdA/+C2A4trRCnD3jS8BB1obq2Gk6GU6QbQAS4BUaYSQAf4bhhKKTFdAzrAOwAxEUAH+KEM01SY3gM6wBsEAQB0gJ+maZoC3gI6iPYaAIBJsiRmHU0AALOeFC3aK2cWAACUXe7+AwO0lc9eTHYTAAAAAElFTkSuQmCC') no-repeat;
  background-position: 0px 0px;
}
.tvu span:hover {
  text-decoration: underline;
}
@media screen and (-webkit-min-device-pixel-ratio:0){
  .tvu{
    -webkit-animation: webkit-adjacent-element-selector-bugfix infinite 1s;
  }

  @-webkit-keyframes webkit-adjacent-element-selector-bugfix {
    from { 
      padding: 0;
    } 
    to { 
      padding: 0;
    }
  }
}
#uploader { 
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  height:28px;
  line-height: 24px;
  padding-left: 10px;
  background-color: #444;
  color:#EEE;
}
#tree { 
  position: absolute;
  top: 28px;
  bottom: 0;
  left: 0;
  width:160px;
  padding: 8px;
}
#editor, #preview { 
  position: absolute;
  top: 28px;
  right: 0;
  bottom: 0;
  left: 160px;
  border-left:1px solid #EEE;
}
#preview {
  background-color: #EEE;
  padding:5px;
}
#loader { 
  position: absolute;
  top: 36%;
  right: 40%;
}
.loader {
    z-index: 10000;
    border: 8px solid #b5b5b5; /* Grey */
    border-top: 8px solid #3498db; /* Blue */
    border-bottom: 8px solid #3498db; /* Blue */
    border-radius: 50%;
    width: 240px;
    height: 240px;
    animation: spin 2s linear infinite;
    display:none;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}
</style>
<script>
if (typeof XMLHttpRequest === "undefined") {
  XMLHttpRequest = function () {
    try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e) {}
    try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e) {}
    try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) {}
    throw new Error("This browser does not support XMLHttpRequest.");
  };
}

function ge(a){
  return document.getElementById(a);
}
function ce(a){
  return document.createElement(a);
}

var QueuedRequester = function () {
  this.queue = [];
  this.running = false;
  this.xmlhttp = null;
}
QueuedRequester.prototype = {
  _request: function(req){
    this.running = true;
    if(!req instanceof Object) return;
    var that = this;
    
    function ajaxCb(x,d){ return function(){
      if (x.readyState == 4){
        ge("loader").style.display = "none";
        d.callback(x.status, x.responseText);
        if(that.queue.length === 0) that.running = false;
        if(that.running) that._request(that.queue.shift());
      }
    }}
    
    ge("loader").style.display = "block";
    
    var p = "";
    if(req.params instanceof FormData){
      p = req.params;
    } else if(req.params instanceof Object){
      for (var key in req.params) {
        if(p === "")
          p += (req.method === "GET")?"?":"";
        else
          p += "&";
        p += encodeURIComponent(key+"="+req.params[key]);
      };
    }
    
    this.xmlhttp = new XMLHttpRequest();
    this.xmlhttp.onreadystatechange = ajaxCb(this.xmlhttp, req);
    if(req.method === "GET"){
      this.xmlhttp.open(req.method, req.url+p, true);
      this.xmlhttp.send();
    } else {
      this.xmlhttp.open(req.method, req.url, true);
      if(p instanceof String)
        this.xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
      this.xmlhttp.send(p);
    }
  },
  stop: function(){
    if(this.running) this.running = false;
    if(this.xmlhttp && this.xmlhttp.readyState < 4){
      this.xmlhttp.abort();
    }
  },
  add: function(method, url, params, callback){
    this.queue.push({url:url,method:method,params:params,callback:callback});
    if(!this.running){
      this._request(this.queue.shift());
    }
  }
}

var requests = new QueuedRequester();

function createFileUploader(element, tree, editor){
  var xmlHttp;
  var input = ce("input");
  input.type = "file";
  input.multiple = false;
  input.name = "data";
  ge(element).appendChild(input);
  var path = ce("input");
  path.id = "upload-path";
  path.type = "text";
  path.name = "path";
  path.defaultValue = "/";
  ge(element).appendChild(path);
  var button = ce("button");
  button.innerHTML = 'Upload';
  ge(element).appendChild(button);
  var mkfile = ce("button");
  mkfile.innerHTML = 'Create';
  ge(element).appendChild(mkfile);

  var savefile = ce("button");
  savefile.innerHTML = ' Save ' ;
  ge(element).appendChild(savefile);

  function httpPostProcessRequest(status, responseText){
    if(status != 200)
      alert("ERROR["+status+"]: "+responseText);
    else
      tree.refreshPath(path.value);
  }
  function createPath(p){
    var formData = new FormData();
    formData.append("path", p);
    requests.add("PUT", "/edit", formData, httpPostProcessRequest);
  }
  
  mkfile.onclick = function(e){
    if(path.value.indexOf(".") === -1) return;
    createPath(path.value);
    editor.loadUrl(path.value);
  };

  savefile.onclick = function(e){
    editor.execCommand('saveCommand');
  };
  
  button.onclick = function(e){
    if(input.files.length === 0){
      return;
    }
    var formData = new FormData();
    formData.append("data", input.files[0], path.value);
    requests.add("POST", "/edit", formData, httpPostProcessRequest);
  };
  input.onchange = function(e){
    if(input.files.length === 0) return;
    var filename = input.files[0].name;
    var ext = /(?:\.([^.]+))?$/.exec(filename)[1];
    var name = /(.*)\.[^.]+$/.exec(filename)[1];
    if(typeof name !== undefined){
      filename = name;
    }
    if(typeof ext !== undefined){
      if(ext === "html") ext = "htm";
      else if(ext === "jpeg") ext = "jpg";
      filename = filename + "." + ext;
    }
    if(path.value === "/" || path.value.lastIndexOf("/") === 0){
      path.value = "/"+filename;
    } else {
      path.value = path.value.substring(0, path.value.lastIndexOf("/")+1)+filename;
    }
  };
}

function createTree(element, editor){
  var preview = ge("preview");
  var treeRoot = ce("div");
  treeRoot.className = "tvu";
  ge(element).appendChild(treeRoot);

  function loadDownload(path){
    ge('download-frame').src = "/edit?download="+path;
  }

  function loadPreview(path){
    ge("editor").style.display = "none";
    preview.style.display = "block";
    preview.innerHTML = '<img src="/edit?edit='+path+'&_cb='+Date.now()+'" style="max-width:100%; max-height:100%; margin:auto; display:block;" />';
  }

  function fillFileMenu(el, path){
    var list = ce("ul");
    el.appendChild(list);
    var action = ce("li");
    list.appendChild(action);
    if(isImageFile(path)){
      action.innerHTML = "<span>Preview</span>";
      action.onclick = function(e){
        loadPreview(path);
        if(document.body.getElementsByClassName('cm').length > 0) document.body.removeChild(el);
      };
    } else if(isTextFile(path)){
      action.innerHTML = "<span>Edit</span>";
      action.onclick = function(e){
        editor.loadUrl(path);
        if(document.body.getElementsByClassName('cm').length > 0) document.body.removeChild(el);
      };
    }
    var download = ce("li");
    list.appendChild(download);
    download.innerHTML = "<span>Download</span>";
    download.onclick = function(e){
      loadDownload(path);
      if(document.body.getElementsByClassName('cm').length > 0) document.body.removeChild(el);
    };
    var delFile = ce("li");
    list.appendChild(delFile);
    delFile.innerHTML = "<span>Delete</span>";
    delFile.onclick = function(e){
      httpDelete(path);
      if(document.body.getElementsByClassName('cm').length > 0) document.body.removeChild(el);
    };
  }

  function showContextMenu(event, path, isfile){
    var divContext = ce("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 = event.clientX + scrollLeft;
    var top = event.clientY + scrollTop;
    divContext.className = 'cm';
    divContext.style.display = 'block';
    divContext.style.left = left + 'px';
    divContext.style.top = top + 'px';
    fillFileMenu(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('cm').length > 0) document.body.removeChild(divContext);
      }
    };
  }

  function createTreeLeaf(path, name, size){
    var leaf = ce("li");
    leaf.id = (((path == "/")?"":path)+"/"+name);
    var label = ce("span");
    label.innerHTML = name;
    leaf.appendChild(label);
    leaf.onclick = function(e){
      if(isTextFile(leaf.id.toLowerCase())){
        editor.loadUrl(leaf.id);
      } else if(isImageFile(leaf.id.toLowerCase())){
        loadPreview(leaf.id);
      }
    };
    leaf.oncontextmenu = function(e){
      e.preventDefault();
      e.stopPropagation();
      showContextMenu(e, leaf.id, true);
    };
    return leaf;
  }

  function addList(parent, path, items){
    var list = ce("ul");
    parent.appendChild(list);
    var ll = items.length;
    for(var i = 0; i < ll; i++){
      if(items[i].type === "file")
        list.appendChild(createTreeLeaf(path, items[i].name, items[i].size));
    }

  }

  function isTextFile(path){
    var ext = /(?:\.([^.]+))?$/.exec(path)[1];
    if(typeof ext !== undefined){
      switch(ext){
        case "txt":
        case "htm":
        case "html":
        case "js":
        case "css":
        case "xml":
        case "json":
        case "conf":
        case "ini":
        case "h":
        case "c":
        case "cpp":
        case "php":
        case "hex":
          return true;
      }
    }
    return false;
  }

  function isImageFile(path){
    var ext = /(?:\.([^.]+))?$/.exec(path)[1];
    if(typeof ext !== undefined){
      switch(ext){
        case "png":
        case "jpg":
        case "gif":
          return true;
      }
    }
    return false;
  }

  this.refreshPath = function(path){
    treeRoot.removeChild(treeRoot.childNodes[0]);
    httpGet(treeRoot, "/");
  };

  function delCb(path){
    return function(status, responseText){
      if(status != 200){
        alert("ERROR["+status+"]: "+responseText);
      } else {
        treeRoot.removeChild(treeRoot.childNodes[0]);
        httpGet(treeRoot, "/");
      }
    }
  }

  function httpDelete(filename){
    var formData = new FormData();
    formData.append("path", filename);
    requests.add("DELETE", "/edit", formData, delCb(filename));
  }

  function getCb(parent, path){
    return function(status, responseText){
      if(status == 200)
        addList(parent, path, JSON.parse(responseText));
    }
  }

  function httpGet(parent, path){
    requests.add("GET", "/edit", { list: path }, getCb(parent, path));
  }

  httpGet(treeRoot, "/");
  return this;
}

function createEditor(element, file, lang, theme, type){
  function getLangFromFilename(filename){
    var lang = "plain";
    var ext = /(?:\.([^.]+))?$/.exec(filename)[1];
    if(typeof ext !== undefined){
      switch(ext){
        case "txt": lang = "plain"; break;
        case "hex": lang = "plain"; break;
        case "conf": lang = "plain"; break;
        case "htm": lang = "html"; break;
        case "js": lang = "javascript"; break;
        case "h": lang = "c_cpp"; 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":
        case "ini":
          lang = ext;
      }
    }
    return lang;
  }

  if(typeof file === "undefined") file = "/index.htm";

  if(typeof lang === "undefined"){
    lang = getLangFromFilename(file);
  }

  if(typeof theme === "undefined") theme = "monokai";

  if(typeof type === "undefined"){
    type = "text/"+lang;
    if(lang === "c_cpp") type = "text/plain";
  }

  var editor = ace.edit(element);
  function httpPostProcessRequest(status, responseText){
    if(status != 200) alert("ERROR["+status+"]: "+responseText);
  }
  function httpPost(filename, data, type){
    var formData = new FormData();
    formData.append("data", new Blob([data], { type: type }), filename);
    requests.add("POST", "/edit", formData, httpPostProcessRequest);
  }
  function httpGetProcessRequest(status, responseText){
      ge("preview").style.display = "none";
      ge("editor").style.display = "block";
      if(status == 200)
        editor.setValue(responseText);
      else
        editor.setValue("");
      editor.clearSelection();
  }
  function httpGet(theUrl){
      requests.add("GET", "/edit", { edit: theUrl }, httpGetProcessRequest);
  }

  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: 'saveCommand',
      bindKey: {win: 'Ctrl-S',  mac: 'Command-S'},
      exec: function(editor) {
        httpPost(file, editor.getValue()+"", type);
      },
      readOnly: false
  });
  editor.commands.addCommand({
      name: 'undoCommand',
      bindKey: {win: 'Ctrl-Z',  mac: 'Command-Z'},
      exec: function(editor) {
        editor.getSession().getUndoManager().undo(false);
      },
      readOnly: false
  });
  editor.commands.addCommand({
      name: 'redoCommand',
      bindKey: {win: 'Ctrl-Shift-Z',  mac: 'Command-Shift-Z'},
      exec: function(editor) {
        editor.getSession().getUndoManager().redo(false);
      },
      readOnly: false
  });
  httpGet(file);
  editor.loadUrl = function(filename){
    file = filename;
    lang = getLangFromFilename(file);
    type = "text/"+lang;
    if(lang !== "plain") editor.getSession().setMode("ace/mode/"+lang);
    httpGet(file);
  };
  return editor;
}
function onBodyLoad(){
  var vars = {};
  var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) { vars[key] = value; });
  var editor = createEditor("editor", vars.file, vars.lang, vars.theme);
  var tree = createTree("tree", editor);
  createFileUploader("uploader", tree, editor);
};
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.3/ace.js" type="text/javascript" charset="utf-8"></script>
</head>
<body onload="onBodyLoad();">
  <div id="loader" class="loader"></div>
  <div id="uploader"></div>
  <div id="tree"></div>
  <div id="editor"></div>
  <div id="preview" style="display:none;"></div>
  <iframe id=download-frame style='display:none;'></iframe>
</body>
</html>

@omersiar
Copy link
Contributor

@hallard

46663564

@stale
Copy link

stale bot commented Sep 21, 2019

[STALE_SET] This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 14 days if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Sep 21, 2019
@stale
Copy link

stale bot commented Oct 5, 2019

[STALE_DEL] This stale issue has been automatically closed. Thank you for your contributions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants