[FIX] tools: adapt playground to owl 2

This commit is contained in:
Lucas Perais (lpe)
2021-11-17 18:41:01 +01:00
committed by Géry Debongnie
parent dabc24cee3
commit efd934d2b1
6 changed files with 573 additions and 706 deletions
+76 -169
View File
@@ -1,19 +1,11 @@
import { SAMPLES } from "./samples.js"; import { SAMPLES } from "./samples.js";
const { mount, hooks } = owl; import { debounce, loadJS } from "./utils.js";
const { useState, useRef, onMounted, onWillUnmount } = hooks; import { exportStandaloneApp } from "./export_snippet.js";
const { useState, useRef, onMounted, onWillUnmount, onPatched, onWillUpdateProps } = owl;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Constants, helpers, utils // 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 = { const MODES = {
js: "ace/mode/javascript", js: "ace/mode/javascript",
@@ -24,77 +16,41 @@ const MODES = {
const DEFAULT_XML = `<templates> const DEFAULT_XML = `<templates>
</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. * Make an iframe, with all the js, css and xml properly injected.
*/ */
function makeCodeIframe(js, css, xml) { function makeCodeIframe(js, css, xml) {
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "").replace(/`/g, '\\\`'); const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "").replace(/`/g, '\\\`');
// create iframe // create iframe
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");
iframe.onload = () => { iframe.onload = () => {
const doc = iframe.contentDocument; const doc = iframe.contentDocument;
const utilsMod = doc.createElement("script");
utilsMod.setAttribute("type", "module");
utilsMod.setAttribute("src", "utils.js");
doc.head.appendChild(utilsMod);
// inject js // inject js
const owlScript = doc.createElement("script"); const owlScript = doc.createElement("script");
owlScript.type = "text/javascript"; owlScript.type = "text/javascript";
owlScript.src = "../owl.js"; owlScript.src = "../owl.js";
owlScript.addEventListener("load", () => { owlScript.addEventListener("load", () => {
const script = doc.createElement("script"); const script = doc.createElement("script");
script.type = "text/javascript"; script.type = "module";
const content = ` const content = `
{ import * as utils from "./utils.js";
owl.config.mode = 'dev'; (function (owl) {
let templates = \`${sanitizedXML}\`; const _configure = owl.App.prototype.configure;
const qweb = new owl.QWeb({ templates }); owl.App.prototype.configure = function configureOverriden(config) {
owl.Component.env = { qweb }; config = Object.assign({ dev: true }, config);
} this.addTemplates(\`${sanitizedXML}\`);
${js}`; return _configure.call(this, config);
}
})(owl);
(async function() {
${js}
})()`;
script.innerHTML = content; script.innerHTML = content;
doc.body.appendChild(script); doc.body.appendChild(script);
}); });
@@ -108,58 +64,6 @@ function makeCodeIframe(js, css, xml) {
return iframe; 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 = `
/**
* This is the javascript code defined in the playground.
* In a larger application, this code should probably be moved in different
* sub files.
*/
function app() {
${processedJS}
}
/**
* Initialization code
* This code load templates, and make sure everything is properly connected.
*/
async function start() {
let templates;
try {
templates = await owl.utils.loadFile('app.xml');
} catch(e) {
console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
return;
}
const env = { qweb: new owl.QWeb({templates})};
owl.Component.env = env;
await owl.utils.whenReady();
app();
}
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 // SAMPLES
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -189,7 +93,7 @@ function deleteLocalSample() {
function useSamples() { function useSamples() {
const samples = loadSamples(); const samples = loadSamples();
const component = owl.Component.current; const component = owl.useComponent();
let interval; let interval;
onMounted(() => { onMounted(() => {
@@ -205,57 +109,57 @@ function useSamples() {
}); });
return samples; return samples;
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Tabbed editor // Tabbed editor
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class TabbedEditor extends owl.Component { class TabbedEditor extends owl.Component {
constructor(parent, props) { setup() {
super(parent, props); const props = this.props;
this.state = useState({ this.state = useState({
currentTab: props.js !== false ? "js" : props.xml ? "xml" : "css" currentTab: props.js !== false ? "js" : props.xml ? "xml" : "css"
}); });
this.setTab = owl.utils.debounce(this.setTab, 250, true); this.setTab = debounce(this.setTab.bind(this), 250, true);
this.sessions = {}; this.sessions = {};
this._setupSessions(props); this._setupSessions(props);
this.editorNode = useRef("editor"); this.editorNode = useRef("editor");
this._updateCode = this._updateCode.bind(this); this._updateCode = this._updateCode.bind(this);
}
mounted() { onMounted(() => {
this.editor = this.editor || ace.edit(this.editorNode.el); this.editor = this.editor || ace.edit(this.editorNode.el);
this.editor.setValue(this.props[this.state.currentTab], -1); this.editor.setValue(this.props[this.state.currentTab], -1);
this.editor.setFontSize("12px"); this.editor.setFontSize("12px");
this.editor.setTheme("ace/theme/monokai"); this.editor.setTheme("ace/theme/monokai");
this.editor.setSession(this.sessions[this.state.currentTab]); this.editor.setSession(this.sessions[this.state.currentTab]);
const tabSize = this.state.currentTab === "xml" ? 2 : 4; const tabSize = this.state.currentTab === "xml" ? 2 : 4;
this.editor.session.setOption("tabSize", tabSize); this.editor.session.setOption("tabSize", tabSize);
this.editor.on("blur", this._updateCode); this.editor.on("blur", this._updateCode);
this.interval = setInterval(this._updateCode, 3000); this.interval = setInterval(this._updateCode, 3000);
} });
willUnmount() { onPatched(() => {
clearInterval(this.interval); const session = this.sessions[this.state.currentTab];
this.editor.off("blur", this._updateCode); let content = this.props[this.state.currentTab];
} if (content === false) {
willUpdateProps(nextProps) { const tab = this.props.js !== false ? "js" : this.props.xml ? "xml" : "css";
this._setupSessions(nextProps); content = this.props[tab];
} this.state.currentTab = tab;
}
if (this.editor.getValue() !== content) {
session.setValue(content, -1);
this.editor.setSession(session);
this.editor.resize();
}
});
patched() { onWillUpdateProps((nextProps) => this._setupSessions(nextProps));
const session = this.sessions[this.state.currentTab];
let content = this.props[this.state.currentTab]; onWillUnmount(() => {
if (content === false) { clearInterval(this.interval);
const tab = this.props.js !== false ? "js" : this.props.xml ? "xml" : "css"; this.editor.off("blur", this._updateCode);
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) { setTab(tab) {
@@ -298,20 +202,20 @@ class TabbedEditor extends owl.Component {
const editorValue = this.editor.getValue(); const editorValue = this.editor.getValue();
const propsValue = this.props[this.state.currentTab]; const propsValue = this.props[this.state.currentTab];
if (editorValue !== propsValue) { if (editorValue !== propsValue) {
this.trigger("updateCode", { this.props.updateCode({
type: this.state.currentTab, type: this.state.currentTab,
value: editorValue value: editorValue
}); });
} }
} }
} }
TabbedEditor.template = "TabbedEditor";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// MAIN APP // MAIN APP
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class App extends owl.Component { class App extends owl.Component {
constructor(...args) { setup() {
super(...args);
this.version = owl.__info__.version; this.version = owl.__info__.version;
this.SAMPLES = useSamples(); this.SAMPLES = useSamples();
this.isDirty = false; this.isDirty = false;
@@ -326,10 +230,11 @@ class App extends owl.Component {
topPanelHeight: null topPanelHeight: null
}); });
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true); this.toggleLayout = debounce(this.toggleLayout, 250, true);
this.runCode = owl.utils.debounce(this.runCode, 250, true); this.runCode = debounce(this.runCode, 250, true);
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true); this.downloadCode = debounce(this.downloadCode, 250, true);
this.content = useRef("content"); this.content = useRef("content");
this.updateCode = this.updateCode.bind(this);
} }
runCode() { runCode() {
@@ -379,9 +284,9 @@ class App extends owl.Component {
} }
}); });
} }
updateCode(ev) { updateCode({type, value}) {
if (this.state[ev.detail.type] !== ev.detail.value) { if (this.state[type] !== value) {
this.state[ev.detail.type] = ev.detail.value; this.state[type] = value;
this.isDirty = true; this.isDirty = true;
} }
} }
@@ -401,12 +306,13 @@ class App extends owl.Component {
async downloadCode() { async downloadCode() {
const { js, css, xml } = this.state; const { js, css, xml } = this.state;
const content = await makeApp(js, css, xml); const content = await exportStandaloneApp(js, css, xml);
await owl.utils.loadJS("libs/FileSaver.min.js"); await loadJS("libs/FileSaver.min.js");
saveAs(content, "app.zip"); saveAs(content, "app.zip");
} }
} }
App.components = { TabbedEditor }; App.components = { TabbedEditor };
App.template = "App";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Application initialization // Application initialization
@@ -416,12 +322,13 @@ async function start() {
const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`; const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`;
console.info(`This application is using Owl built with the following commit:`, commit); console.info(`This application is using Owl built with the following commit:`, commit);
const [templates] = await Promise.all([ const [templates] = await Promise.all([
owl.utils.loadFile("templates.xml"), owl.loadFile("templates.xml"),
owl.utils.whenReady() owl.whenReady()
]); ]);
const qweb = new owl.QWeb({ templates }); const rootApp = new owl.App(App);
const env = { qweb }; rootApp.addTemplates(templates);
await mount(App, {target: document.body, env});
await rootApp.mount(document.body);
} }
start(); start();
+123
View File
@@ -0,0 +1,123 @@
import { loadJS } from "./utils.js";
const sourceCache = new Map();
async function getSourceCode(filePath) {
if (sourceCache.has(filePath)) {
return sourceCache.get(filePath);
}
let source = await fetch(`${filePath}.js`);
source = await source.text();
sourceCache.set(filePath, source);
return source;
}
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 type="module" src="app.js"></script>
<script type="module" src="utils.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 a zip file containing a functioning application
*/
export async function exportStandaloneApp(js, css, xml) {
await loadJS("libs/jszip.min.js");
const zip = new JSZip();
const processedJS = js
.split("\n")
.map(l => (l === "" ? "" : " " + l))
.join("\n");
const JS = `
/**
* This is the javascript code defined in the playground.
* In a larger application, this code should probably be moved in different
* sub files.
*/
import * as utils from "./utils.js";
async function app() {
${processedJS}
}
/**
* Initialization code
* This code load templates, and make sure everything is properly connected.
*/
function prepareOWLApp(templates) {
const _configure = owl.App.prototype.configure;
owl.App.prototype.configure = function configureOverriden(config) {
config = Object.assign({ dev: true }, config);
this.addTemplates(templates);
return _configure.call(this, config);
}
}
async function start() {
let templates;
try {
templates = await owl.loadFile('app.xml');
} catch(e) {
console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
return;
}
prepareOWLApp(templates);
app();
}
start();
`;
const [owlSrc, utilsSrc] = await Promise.all([
getSourceCode("../owl"),
getSourceCode("./utils"),
]);
zip.file("app.js", JS);
zip.file("utils.js", utilsSrc);
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", owlSrc);
return zip.generateAsync({ type: "blob" });
}
+2
View File
@@ -12,6 +12,8 @@
<!-- Application JS/CSS --> <!-- Application JS/CSS -->
<link rel="stylesheet" href="playground.css"> <link rel="stylesheet" href="playground.css">
<script type="module" src="export_snippet.js"></script>
<script type="module" src="utils.js"></script>
<script type="module" src="app.js"></script> <script type="module" src="app.js"></script>
</head> </head>
File diff suppressed because it is too large Load Diff
+7 -6
View File
@@ -1,8 +1,8 @@
<templates> <templates>
<div t-name="TabbedEditor" class="tabbed-editor"> <div t-name="TabbedEditor" class="tabbed-editor">
<div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown"> <div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown">
<t t-foreach="['js', 'xml', 'css']" t-as="tab"> <t t-foreach="['js', 'xml', 'css']" t-as="tab" t-key="tab">
<a t-ref="{{tab}}" t-if="props[tab] !== false" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="setTab(tab)"> <a t-ref="{{tab}}" t-if="props[tab] !== false" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="() => setTab(tab)">
<t t-esc="tab"/> <t t-esc="tab"/>
</a> </a>
</t> </t>
@@ -12,8 +12,7 @@
<div t-name="App" class="playground"> <div t-name="App" class="playground">
<div class="left-bar" t-att-class="{split: state.splitLayout}" <div class="left-bar" t-att-class="{split: state.splitLayout}"
t-att-style="leftPaneStyle" t-att-style="leftPaneStyle">
t-on-updateCode="updateCode">
<div class="menubar"> <div class="menubar">
<a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a> <a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a>
<select t-on-change="setSample"> <select t-on-change="setSample">
@@ -28,7 +27,8 @@
js="state.js" js="state.js"
css="!state.splitLayout and state.css" css="!state.splitLayout and state.css"
xml="!state.splitLayout and state.xml" xml="!state.splitLayout and state.xml"
t-att-style="topEditorStyle"/> style="topEditorStyle"
updateCode="updateCode"/>
<t t-if="state.splitLayout"> <t t-if="state.splitLayout">
<div class="separator horizontal"/> <div class="separator horizontal"/>
<TabbedEditor <TabbedEditor
@@ -36,7 +36,8 @@
css="state.css" css="state.css"
xml="state.xml" xml="state.xml"
resizeable="true" resizeable="true"
t-on-updatePanelHeight="updatePanelHeight"/> onUpdatePanelHeight="updatePanelHeight"
updateCode="updateCode"/>
</t> </t>
</div> </div>
<div class="separator vertical" t-on-mousedown="onMouseDown"/> <div class="separator vertical" t-on-mousedown="onMouseDown"/>
+49
View File
@@ -0,0 +1,49 @@
export function debounce(func, wait, immediate) {
let timeout;
return function () {
const context = this;
const args = arguments;
function later() {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
}
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
}
export function useBus(bus, evName, cb) {
bus.addEventListener(evName, cb);
owl.onWillUnmount(() => {
bus.removeEventListener(evName, cb);
});
}
const loadedScripts = {};
export function loadJS(url) {
if (url in loadedScripts) {
return loadedScripts[url];
}
const promise = new Promise(function (resolve, reject) {
const script = document.createElement("script");
script.type = "text/javascript";
script.src = url;
script.onload = function () {
resolve();
};
script.onerror = function () {
reject(`Error loading file '${url}'`);
};
const head = document.head || document.getElementsByTagName("head")[0];
head.appendChild(script);
});
loadedScripts[url] = promise;
return promise;
}