cotestatnt / esp-fs-webserver

ESP32/ESP8266 webserver, WiFi manager and web editor Arduino library
MIT License
117 stars 28 forks source link

ESP8266 / ESP32 webserver ace js tryit alike live preview #14

Closed ldijkman closed 2 years ago

ldijkman commented 2 years ago

hello

i am using ace on ESP8266 / ESP32 webserver for wifi relais switches i would like to ad live html preview to the ace code text editor (tryit codepen jsfiddle alike)

like https://github.com/geard-dev/web-based-live-code-editor demo https://live-editor.js.org/html.html

but i do not now how to implement it in my editor code

is there anyone that can help me?

Greet Luberth https://github.com/ldijkman/randomnerd_esp32_wifi_manager/tree/main/LAB_Experiments

<!DOCTYPE html>
<html lang="en">
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Electra's ESP Editor</title>
<link rel="apple-touch-icon" href="/ace.ico" type="image/x-icon">
<link rel="shortcut icon" href="/ace.ico" type="image/x-icon">
<link rel="icon" href="/ace.ico" type="image/x-icon">
<style type="text/css" media="screen">
  label {
    font-size: 12px;
    font-family: sans-serif
  }

  .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: 700
  }

  .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 li,
  .tvu ul {
    padding: 0;
    margin: 0;
    list-style: none
  }

  .tvu input {
    position: absolute;
    opacity: 0
  }

  .tvu {
    font: 400 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: 0 0
  }

  .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;
    border-top: 8px solid #3498db;
    border-bottom: 8px solid #3498db;
    border-radius: 50%;
    width: 240px;
    height: 240px;
    animation: spin 2s linear infinite;
    display: none
  }

  @keyframes spin {
    0% {
      transform: rotate(0)
    }

    100% {
      transform: rotate(360deg)
    }
  }
</style>
<script>
  function ge(e) {
    return document.getElementById(e)
  }

  function ce(e) {
    return document.createElement(e)
  }

  function sortByKey(e, t) {
    return e.sort((function(e, n) {
      var a = e[t],
        i = n[t];
      return i > a ? -1 : a > i ? 1 : 0
    }))
  }

  function createFileUploader(e, t, n) {
    var a = /(iPhone)*(OS ([7-9]|1[0-1])_)/i.test(navigator.userAgent);

    function i(e, n) {
      200 != e ? alert("ERROR[" + e + "]: " + n) : t.refreshPath(d.value)
    }
    var o = ce("button");
    o.innerHTML = "Root Dir", ge(e).appendChild(o);
    var c = ce("input");
    c.type = "file", c.multiple = !1, c.name = "data", c.id = "upload-select", ge(e).appendChild(c);
    var d = ce("input");
    d.id = "upload-path", d.type = "text", d.name = "path", d.defaultValue = "/", ge(e).appendChild(d);
    var s = ce("button");
    s.innerHTML = "Upload", ge(e).appendChild(s);
    var r = ce("button");
    r.innerHTML = "Create", ge(e).appendChild(r);
    var l = ce("input");
    l.id = "editor-filename", l.type = "text", l.disabled = !0, l.size = 20, ge(e).appendChild(l);
    var u = ce("input");
    u.id = "ipad-fix", u.name = "ipad-fix", u.type = "checkbox", u.checked = !!a;
    var p = ce("label");
    p.for = u.id, p.innerHTML = " Alt.";
    var m = ce("button");
    m.innerHTML = " Save ", ge(e).appendChild(m), ge(e).appendChild(p), ge(e).appendChild(u), r.onclick = function(e) {
      (function(e) {
        var t = new FormData;
        t.append("path", e), requests.add("PUT", "/edit", t, i)
      })(d.value), n.loadUrl(d.value), d.value = "/"
    }, m.onclick = function(e) {
      if (u.checked) {
        var t = ace.edit("editor").getValue(),
          a = new FormData;
        a.append("rawname", l.value);
        var o = 0;
        const e = 4096;
        for (var c = 0; c < t.length; c += e) {
          var d = t.substring(c, c + e);
          a.append("raw" + o, d), o++
        }
        requests.add("POST", "/edit", a, i)
      } else n.execCommand("saveCommand")
    }, o.onclick = function(e) {
      t.refreshPath(d.value)
    }, s.onclick = function(e) {
      if (0 !== c.files.length) {
        var t = new FormData;
        t.append("data", c.files[0], d.value), requests.add("POST", "/edit", t, i), ge("upload-path").value = "/", ge("upload-select").value = ""
      }
    }, c.onchange = function(e) {
      if (0 !== c.files.length) {
        var t = c.files[0].name,
          n = /(?:\.([^.]+))?$/.exec(t)[1],
          a = /(.*)\.[^.]+$/.exec(t)[1];
        void 0 !== typeof a && (t = a), d.value = "/" + t + "." + n
      }
    }
  }

  function createTree(e, t) {
    function n(e) {
      ge("editor-filename").value = e, ge("editor").style.display = "none", p.style.display = "block", p.innerHTML = '<img src="/edit?edit=' + e + "&_cb=" + Date.now() + '" style="max-width:100%; max-height:100%; margin:auto; display:block;" />'
    }

    function a(e, a) {
      var i = ce("ul");
      e.appendChild(i);
      var o = ce("li");
      i.appendChild(o), s(a) ? (o.innerHTML = "<span>Preview</span>", o.onclick = function(t) {
        n(a), document.body.getElementsByClassName("cm").length > 0 && document.body.removeChild(e)
      }) : d(a) && (o.innerHTML = "<span>Edit</span>", o.onclick = function(n) {
        t.loadUrl(a), document.body.getElementsByClassName("cm").length > 0 && document.body.removeChild(e)
      });
      var l = ce("li");
      i.appendChild(l), d(a) || s(a) || function(e) {
        var t = /(?:.([^.]+))?$/.exec(e)[1];
        if (void 0 !== typeof t) switch (t) {
          case "ico":
          case "gz":
          case "zip":
          case "wav":
          case "mp3":
          case "pdf":
            return !0
        }
        return !1
      }(a) ? (l.innerHTML = "<span>Download</span>", l.onclick = function(t) {
        (function(e) {
          ge("download-frame").src = "/edit?download=" + e
        })(a), document.body.getElementsByClassName("cm").length > 0 && document.body.removeChild(e)
      }) : c(a) && (i.appendChild(l), l.innerHTML = "<span>ChDir</span>", l.onclick = function(t) {
        m.removeChild(m.childNodes[0]), u(m, a), document.body.getElementsByClassName("cm").length > 0 && document.body.removeChild(e)
      });
      var p = ce("li");
      i.appendChild(p), p.innerHTML = "<span>Delete</span>", p.onclick = function(t) {
        r(a), document.body.getElementsByClassName("cm").length > 0 && document.body.removeChild(e)
      }
    }

    function i(e, t, n) {
      var i = ce("div"),
        o = document.body.scrollTop ? document.body.scrollTop : document.documentElement.scrollTop,
        c = document.body.scrollLeft ? document.body.scrollLeft : document.documentElement.scrollLeft,
        d = e.clientX + c,
        s = e.clientY + o;
      i.className = "cm", i.style.display = "block", i.style.left = d + "px", i.style.top = s + "px", a(i, t), document.body.appendChild(i);
      var r = i.offsetWidth,
        l = i.offsetHeight;
      i.onmouseout = function(e) {
        (e.clientX < d || e.clientX > d + r || e.clientY < s || e.clientY > s + l) && document.body.getElementsByClassName("cm").length > 0 && document.body.removeChild(i)
      }
    }

    function o(e, a, o) {
      var r = ce("li");
      r.id = a;
      var l = ce("span");
      return l.innerHTML = a, r.appendChild(l), r.onclick = function(e) {
        d(r.id.toLowerCase()) ? t.loadUrl(r.id) : s(r.id.toLowerCase()) ? n(r.id) : c(r.id) && (m.removeChild(m.childNodes[0]) && u(m, r.id.toLowerCase()))
      }, r.oncontextmenu = function(e) {
        e.preventDefault(), e.stopPropagation(), i(e, r.id)
      }, r
    }

    function c(e) {
      return -1 == e.indexOf(".")
    }

    function d(e) {
      var t = /(?:.([^.]+))?$/.exec(e)[1];
      if (void 0 !== typeof t) switch (t) {
        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":
        case "ino":
        case "pde":
          return !0
      }
      return !1
    }

    function s(e) {
      var t = /(?:.([^.]+))?$/.exec(e)[1];
      if (void 0 !== typeof t) switch (t) {
        case "png":
        case "jpg":
        case "gif":
        case "bmp":
          return !0
      }
      return !1
    }

    function r(e) {
      var t = new FormData;
      t.append("path", e), requests.add("DELETE", "/edit", t, (function(e, t) {
        200 != e ? alert("ERROR[" + e + "]: " + t) : (m.removeChild(m.childNodes[0]), u(m, "/"))
      }))
    }

    function l(e, t) {
      return function(t, n) {
        200 == t && function(e, t, n) {
          sortByKey(n, "name");
          var a = ce("ul");
          e.appendChild(a);
          for (var i = n.length, c = 0; i > c; c++) "file" === n[c].type && a.appendChild(o(0, n[c].name, n[c].size))
        }(e, 0, JSON.parse(n))
      }
    }

    function u(e, t) {
      requests.add("GET", "/edit", {
        list: t
      }, l(e))
    }
    var p = ge("preview"),
      m = ce("div");
    return m.className = "tvu", ge(e).appendChild(m), this.refreshPath = function(e) {
      m.removeChild(m.childNodes[0]), u(m, "/")
    }, u(m, "/"), this
  }

  function createEditor(e, t, n, a, i) {
    function o(e) {
      var t = "plain",
        n = /(?:.([^.]+))?$/.exec(e)[1];
      if (void 0 !== typeof n) switch (n) {
        case "txt":
        case "hex":
        case "conf":
          t = "plain";
          break;
        case "htm":
          t = "html";
          break;
        case "js":
          t = "javascript";
          break;
        case "h":
        case "c":
        case "cpp":
          t = "c_cpp";
          break;
        case "css":
        case "scss":
        case "php":
        case "html":
        case "json":
        case "xml":
        case "ini":
          t = n
      }
      return t
    }

    function c(e, t) {
      200 != e && alert("ERROR[" + e + "]: " + t)
    }

    function d(e, t) {
      ge("preview").style.display = "none", ge("editor").style.display = "block", 200 == e ? s.setValue(t) : s.setValue(""), s.clearSelection()
    }
    void 0 === t && (t = "/my_edit.html"), void 0 === n && (n = o(t)), void 0 === a && (a = "monokai"), void 0 === i && (i = "text/" + n, "c_cpp" === n && (i = "text/plain"));
    var s = ace.edit(e);
    return "plain" !== n && s.getSession().setMode("ace/mode/" + n), s.setTheme("ace/theme/" + a), s.$blockScrolling = 1 / 0, s.getSession().setUseSoftTabs(!0), s.getSession().setTabSize(2), s.getSession().setUseWorker(!0), s.setHighlightActiveLine(!0), s.setShowPrintMargin(!1), s.commands.addCommand({
      name: "saveCommand",
      bindKey: {
        win: "Ctrl-S",
        mac: "Command-S"
      },
      exec: function(e) {
        ! function(e, t, n) {
          var a = new FormData;
          a.append("data", new Blob([t], {
            type: n
          }), e), requests.add("POST", "/edit", a, c)
        }(t, e.getValue() + "", i)
      },
      readOnly: !1
    }), s.commands.addCommand({
      name: "undoCommand",
      bindKey: {
        win: "Ctrl-Z",
        mac: "Command-Z"
      },
      exec: function(e) {
        e.getSession().getUndoManager().undo(!1)
      },
      readOnly: !1
    }), s.commands.addCommand({
      name: "redoCommand",
      bindKey: {
        win: "Ctrl-Shift-Z",
        mac: "Command-Shift-Z"
      },
      exec: function(e) {
        e.getSession().getUndoManager().redo(!1)
      },
      readOnly: !1
    }), s.loadUrl = function(e) {
      ge("editor-filename").value = e, n = o(t = e), i = "text/" + n, "plain" !== n && s.getSession().setMode("ace/mode/" + n),
        function(e) {
          requests.add("GET", "/edit", {
            edit: e
          }, d)
        }(t)
    }, s
  }

  function onBodyLoad() {
    var e = {},
      t = (window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, (function(t, n, a) {
        e[n] = a
      })), createEditor("editor", e.file, e.lang, e.theme)),
      n = createTree("tree", t);
    window.define = ace.define, window.require = ace.require, ace.config.set("basePath", "/"), ace.config.set("workerPath", "/"), createFileUploader("uploader", n, t), void 0 === e.file && (e.file = "/my_edit.html"), t.loadUrl(e.file)
  }
  "undefined" == typeof XMLHttpRequest && (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.")
  });
  var QueuedRequester = function() {
    this.queue = [], this.running = !1, this.xmlhttp = null
  };
  QueuedRequester.prototype = {
    _request: function(e) {
      if (this.running = !0, !(!e instanceof Object)) {
        var t = this;
        ge("loader").style.display = "block";
        var n = "";
        if (e.params instanceof FormData) n = e.params;
        else if (e.params instanceof Object)
          for (var a in e.params) n += "" === n ? "GET" === e.method ? "?" : "" : "&", n += encodeURIComponent(a) + "=" + encodeURIComponent(e.params[a]);
        this.xmlhttp = new XMLHttpRequest, this.xmlhttp.onreadystatechange = function(e, n) {
          return function() {
            4 == e.readyState && (ge("loader").style.display = "none", n.callback(e.status, e.responseText), 0 === t.queue.length && (t.running = !1), t.running && t._request(t.queue.shift()))
          }
        }(this.xmlhttp, e), "GET" === e.method ? (this.xmlhttp.open(e.method, e.url + n, !0), this.xmlhttp.send()) : (this.xmlhttp.open(e.method, e.url, !0), n instanceof String && this.xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"), this.xmlhttp.send(n))
      }
    },
    stop: function() {
      this.running && (this.running = !1), this.xmlhttp && this.xmlhttp.readyState < 4 && this.xmlhttp.abort()
    },
    add: function(e, t, n, a) {
      this.queue.push({
        url: t,
        method: e,
        params: n,
        callback: a
      }), this.running || this._request(this.queue.shift())
    }
  };
  var requests = new QueuedRequester
</script>
<script id="ace" src="/acefull.js" charset="utf-8"></script>
<script>
  if (void 0 === ace.edit) {
    var script = document.createElement("script");
    script.src = "/ace.js", script.async = !1, document.head.appendChild(script)
  }
</script>

<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>

above code results in next screenshot (running on ESP32 webserver http://garage.local/edit)

2022-07-09-112711_1920x1080_scrot

would like to have a live html preview panel added like demo on https://github.com/geard-dev/web-based-live-code-editor demo https://live-editor.js.org/html.html

2022-07-09-114956_1920x1080_scrot

cotestatnt commented 2 years ago

Ho @ldijkman This library already has ACE editor included in source. You can use with /edit webpage in order to edit yhe source files directly on the esp filesystem.

Regarding the preview, this feature is not avalaible but in my opinion is also not necessary because you can simply open another browser tab, edit whatever you want in your own webpage and imnediatly check the result reloading page.

ldijkman commented 2 years ago

yes your right i can place two browser windows side by side one with editor and one with html preview

but i think it would be nice like next codepen https://codepen.io/ldijkman/full/LYdZpYp

cotestatnt commented 2 years ago

A preview box like that, I think is certainly useful for quickly trying small snippets of code and not entire web pages (which is why there are services like Codepen or Jsfiddle). Personally I still think it is useless, however you can simply put the files of the repository you have linked into the filesystem of the ESP if you like.

If you want to try entire websites I recommend using a service like CodeSandbox (this is your example) where you can put all the files needed.

This library has the main objective of serve the web pages you upload to the filesystem, this means that the development of these web pages can be done anywhere: online, with your local webserver running on a PC or directly with the ESP.