Files
owl/tools/playground/app.js
T
Géry Debongnie b891ae7de4 [REM] tools: remove error handling in playground
There was some code to attempt to display the error in the right
pane, whenever it occured in the app early phase.

However, it made it much harder to properly handle all cases. It could
silently swallow some errors (if more than one was done), And it was
not behaving the same way in Firefox and in Chrome.
2019-11-13 08:49:25 +01:00

414 lines
11 KiB
JavaScript

import { SAMPLES } from "./samples.js";
const { useState, useRef, onMounted, onWillUnmount } = owl.hooks;
//------------------------------------------------------------------------------
// 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 = `<templates>
</templates>`;
const DEFAULT_HTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL App</title>
<link rel="icon" href="data:,">
<script src="owl.js"></script>
<link rel="stylesheet" href="app.css">
<script src="app.js"></script>
</head>
<body>
</body>
</html>
`;
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) {
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "");
// 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.config.mode = 'dev';
let templates = \`${sanitizedXML}\`;
const qweb = new owl.QWeb({ templates });
owl.Component.env = { qweb };
}
${js}`;
script.innerHTML = content;
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.loadFile('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" });
}
//------------------------------------------------------------------------------
// SAMPLES
//------------------------------------------------------------------------------
function loadSamples() {
let result = SAMPLES.slice();
const localSample = localStorage.getItem("owl-playground-local-sample");
if (localSample) {
const { js, css, xml } = JSON.parse(localSample);
result.unshift({
description: "Local Storage Code",
code: js,
xml,
css
});
}
return result;
}
function saveLocalSample(js, css, xml) {
const str = JSON.stringify({ js, css, xml });
localStorage.setItem("owl-playground-local-sample", str);
}
function deleteLocalSample() {
localStorage.removeItem("owl-playground-local-sample");
}
function useSamples() {
const samples = loadSamples();
const component = owl.Component.current;
let interval;
onMounted(() => {
const state = component.state;
interval = setInterval(() => {
if (component.isDirty) {
saveLocalSample(state.js, state.css, state.xml);
}
}, 1000);
});
onWillUnmount(() => {
clearInterval(interval);
});
return samples;
}
//------------------------------------------------------------------------------
// Tabbed editor
//------------------------------------------------------------------------------
class TabbedEditor extends owl.Component {
constructor(parent, props) {
super(parent, props);
this.state = useState({
currentTab: props.js !== false ? "js" : props.xml ? "xml" : "css"
});
this.setTab = owl.utils.debounce(this.setTab, 250, true);
this.sessions = {};
this._setupSessions(props);
this.editorNode = useRef("editor");
this._updateCode = this._updateCode.bind(this);
}
mounted() {
this.editor = this.editor || ace.edit(this.editorNode.el);
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", this._updateCode);
this.interval = setInterval(this._updateCode, 3000);
}
willUnmount() {
clearInterval(this.interval);
this.editor.off("blur", this._updateCode);
}
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 !== false ? "js" : this.props.xml ? "xml" : "css";
content = this.props[tab];
this.state.currentTab = tab;
}
if (this.editor.getValue() !== content) {
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] !== false && !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());
}
}
}
_updateCode() {
const editorValue = this.editor.getValue();
const propsValue = this.props[this.state.currentTab];
if (editorValue !== propsValue) {
this.trigger("updateCode", {
type: this.state.currentTab,
value: editorValue
});
}
}
}
//------------------------------------------------------------------------------
// MAIN APP
//------------------------------------------------------------------------------
class App extends owl.Component {
constructor(...args) {
super(...args);
this.version = owl.__info__.version;
this.SAMPLES = useSamples();
this.isDirty = false;
this.state = useState({
js: this.SAMPLES[0].code,
css: this.SAMPLES[0].css || "",
xml: this.SAMPLES[0].xml || DEFAULT_XML,
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);
this.content = useRef("content");
}
runCode() {
this.content.el.innerHTML = "";
this.state.displayWelcome = false;
const { js, css, xml } = this.state;
const subiframe = makeCodeIframe(js, css, xml);
this.content.el.appendChild(subiframe);
}
setSample(ev) {
const sample = this.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;
deleteLocalSample();
this.isDirty = false;
}
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) {
if (this.state[ev.detail.type] !== ev.detail.value) {
this.state[ev.detail.type] = ev.detail.value;
this.isDirty = true;
}
}
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");
}
}
App.components = { TabbedEditor };
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
document.title = `${document.title} (v${owl.__info__.version})`;
const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`;
console.info(`This application is using Owl built with the following commit:`, commit);
const [templates] = await Promise.all([
owl.utils.loadFile("templates.xml"),
owl.utils.whenReady()
]);
const qweb = new owl.QWeb({ templates });
owl.Component.env = { qweb };
const app = new App();
app.mount(document.body);
}
start();