import { SAMPLES } from "./samples.js"; //------------------------------------------------------------------------------ // Constants, helpers, utils //------------------------------------------------------------------------------ let owlJS; async function owlSourceCode() { if (owlJS) { return owlJS; } const result = await fetch("../owl.js"); owlJS = await result.text(); return owlJS; } const MODES = { js: "ace/mode/javascript", css: "ace/mode/css", xml: "ace/mode/xml" }; const DEFAULT_XML = ` `; const DEFAULT_HTML = ` OWL App `; const APP_PY = `#!/usr/bin/env python3 import threading import time from http.server import SimpleHTTPRequestHandler, HTTPServer def start_server(): SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript' httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler) httpd.serve_forever() url = 'http://127.0.0.1:3600' if __name__ == "__main__": print("Owl Application") print("---------------") print("Server running on: {}".format(url)) threading.Thread(target=start_server, daemon=True).start() while True: try: time.sleep(1) except KeyboardInterrupt: httpd.server_close() quit(0) `; /** * Make an iframe, with all the js, css and xml properly injected. */ function makeCodeIframe(js, css, xml, errorHandler) { // check templates var qweb = new owl.QWeb(); const sanitizedXML = xml.replace(//g, ""); // will throw error if there is something wrong with xml qweb.addTemplates(sanitizedXML); // create iframe const iframe = document.createElement("iframe"); iframe.onload = () => { const doc = iframe.contentDocument; // inject js const owlScript = doc.createElement("script"); owlScript.type = "text/javascript"; owlScript.src = "../owl.js"; owlScript.addEventListener("load", () => { const script = doc.createElement("script"); script.type = "text/javascript"; const content = `owl.__info__.mode = 'dev';\nwindow.TEMPLATES = \`${sanitizedXML}\`\n${js}`; script.innerHTML = content; iframe.contentWindow.addEventListener("error", errorHandler); iframe.contentWindow.addEventListener("unhandledrejection", errorHandler); setTimeout(function() { if (iframe.contentWindow) { iframe.contentWindow.removeEventListener("error", errorHandler); iframe.contentWindow.removeEventListener("unhandledrejection", errorHandler); } }, 200); doc.body.appendChild(script); }); doc.head.appendChild(owlScript); // inject css const style = document.createElement("style"); style.innerHTML = css; doc.head.appendChild(style); }; return iframe; } /** * Make a zip file containing a functioning application */ async function makeApp(js, css, xml) { await owl.utils.loadJS("libs/jszip.min.js"); const zip = new JSZip(); const processedJS = js .split("\n") .map(l => (l === "" ? "" : " " + l)) .join("\n"); const JS = ` async function loadTemplates() { try { return owl.utils.loadTemplates('app.xml'); } catch(e) { console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`); } } function start([TEMPLATES]) { // Application code ${processedJS} } Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start); `; zip.file("app.js", JS); zip.file("app.css", css); zip.file("app.py", APP_PY); zip.file("app.xml", xml); zip.file("index.html", DEFAULT_HTML); zip.file("owl.js", owlSourceCode()); return zip.generateAsync({ type: "blob" }); } //------------------------------------------------------------------------------ // MAIN APP //------------------------------------------------------------------------------ class App extends owl.Component { constructor(...args) { super(...args); this.version = owl.__info__.version; this.SAMPLES = SAMPLES; this.components = { TabbedEditor }; this.state = { js: SAMPLES[0].code, css: SAMPLES[0].css || "", xml: SAMPLES[0].xml || DEFAULT_XML, error: false, displayWelcome: true, splitLayout: true, leftPaneWidth: Math.ceil(window.innerWidth / 2), topPanelHeight: null }; this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true); this.runCode = owl.utils.debounce(this.runCode, 250, true); this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true); } displayError(error) { this.state.error = error; if (error) { setTimeout(() => { this.refs.content.innerHTML = ""; }); return; } } runCode() { this.state.displayWelcome = false; let subiframe; let error = false; const errorHandler = e => this.displayError(e.message || e.reason.message); try { const { js, css, xml } = this.state; subiframe = makeCodeIframe(js, css, xml, errorHandler); } catch (e) { //probably problem with the templates error = e; // we still log the error, always useful to have it available console.error(e); } if (error) { this.displayError(error.message); return; } else { this.state.error = false; } this.refs.content.innerHTML = ""; this.refs.content.appendChild(subiframe); } setSample(ev) { const sample = SAMPLES.find(s => s.description === ev.target.value); this.state.js = sample.code; this.state.css = sample.css || ""; this.state.xml = sample.xml || DEFAULT_XML; } get leftPaneStyle() { return `width:${this.state.leftPaneWidth}px`; } get rightPaneStyle() { return `width:${window.innerWidth - 6 - this.state.leftPaneWidth}px`; } get topEditorStyle() { return `flex: 0 0 ${this.state.topPanelHeight}px`; } onMouseDown() { const resizer = ev => { this.state.leftPaneWidth = ev.clientX; }; document.body.addEventListener("mousemove", resizer); for (let iframe of document.getElementsByTagName("iframe")) { iframe.classList.add("disabled"); } document.body.addEventListener("mouseup", () => { document.body.removeEventListener("mousemove", resizer); for (let iframe of document.getElementsByTagName("iframe")) { iframe.classList.remove("disabled"); } }); } updateCode(ev) { this.state[ev.detail.type] = ev.detail.value; } toggleLayout() { this.state.splitLayout = !this.state.splitLayout; } updatePanelHeight(ev) { if (!ev.detail.delta) { return; } let height = this.state.topPanelHeight; if (!height) { height = document.getElementsByClassName("tabbed-editor")[0].clientHeight; } this.state.topPanelHeight = height + ev.detail.delta; } async downloadCode() { const { js, css, xml } = this.state; const content = await makeApp(js, css, xml); await owl.utils.loadJS("libs/FileSaver.min.js"); saveAs(content, "app.zip"); } } //------------------------------------------------------------------------------ // Tabbed editor //------------------------------------------------------------------------------ class TabbedEditor extends owl.Component { constructor(parent, props) { super(parent, props); this.state = { currentTab: props.js ? "js" : props.xml ? "xml" : "css" }; this.setTab = owl.utils.debounce(this.setTab, 250, true); this.sessions = {}; this._setupSessions(props); this.editor = null; } mounted() { this.editor = this.editor || ace.edit(this.refs.editor); this.editor.setValue(this.props[this.state.currentTab], -1); this.editor.setFontSize("12px"); this.editor.setTheme("ace/theme/monokai"); this.editor.setSession(this.sessions[this.state.currentTab]); const tabSize = this.state.currentTab === "xml" ? 2 : 4; this.editor.session.setOption("tabSize", tabSize); this.editor.on("blur", () => { const editorValue = this.editor.getValue(); const propsValue = this.props[this.state.currentTab]; if (editorValue !== propsValue) { this.trigger("updateCode", { type: this.state.currentTab, value: editorValue }); } }); } willUpdateProps(nextProps) { this._setupSessions(nextProps); } patched() { const session = this.sessions[this.state.currentTab]; let content = this.props[this.state.currentTab]; if (content === false) { const tab = this.props.js ? "js" : this.props.xml ? "xml" : "css"; content = this.props[tab]; this.state.currentTab = tab; } session.setValue(content, -1); this.editor.setSession(session); this.editor.resize(); } setTab(tab) { if (this.state.currentTab !== tab) { this.state.currentTab = tab; const session = this.sessions[this.state.currentTab]; session.doc.setValue(this.props[tab], -1); this.editor.setSession(session); } } onMouseDown(ev) { if (ev.target.tagName === "DIV") { let y = ev.clientY; const resizer = ev => { const delta = ev.clientY - y; y = ev.clientY; this.trigger("updatePanelHeight", { delta }); }; document.body.addEventListener("mousemove", resizer); document.body.addEventListener("mouseup", () => { document.body.removeEventListener("mousemove", resizer); }); } } _setupSessions(props) { for (let tab of ["js", "xml", "css"]) { if (props[tab] && !this.sessions[tab]) { this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]); this.sessions[tab].setOption("useWorker", false); const tabSize = tab === "xml" ? 2 : 4; this.sessions[tab].setOption("tabSize", tabSize); this.sessions[tab].setUndoManager(new ace.UndoManager()); } } } } //------------------------------------------------------------------------------ // Application initialization //------------------------------------------------------------------------------ async function start() { document.title = `${document.title} (v${owl.__info__.version})`; const [templates] = await Promise.all([ owl.utils.loadTemplates("templates.xml"), owl.utils.whenReady() ]); const qweb = new owl.QWeb(templates); const app = new App({ qweb }); app.mount(document.body); } start();