mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
9475de4d18
The playground hasn't been touched for a while and a few things were broken before this commit: - Exporting as a standalone web application was broken because it still attempted to load the templates like it was done in owl 1 - Resizing the editor tabs still tried to use `this.trigger` While fixing the export feature, the files needed by the app were factored out of hardcoded strings and into real files, as this makes it easier to maintain, since these files get syntax highlighting. The samples received the same treatment: there is now a `samples` folder containing all the samples, it also contains a jsconfig, giving autocompletion on owl functions while editing the files, and allowing the owl import to look how it would when using owl as a node_module. In the browser, this import is translated to the relative path of the owl module using an import map.
405 lines
12 KiB
JavaScript
405 lines
12 KiB
JavaScript
import { debounce, loadJS } from "./utils.js";
|
|
import {
|
|
App,
|
|
Component,
|
|
useState,
|
|
useRef,
|
|
onMounted,
|
|
onWillUnmount,
|
|
onPatched,
|
|
onWillUpdateProps,
|
|
loadFile as _loadFile,
|
|
whenReady,
|
|
__info__,
|
|
useEffect,
|
|
onWillStart,
|
|
} from "../owl.js";
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Constants, helpers, utils
|
|
//------------------------------------------------------------------------------
|
|
|
|
const MODES = {
|
|
js: "ace/mode/javascript",
|
|
css: "ace/mode/css",
|
|
xml: "ace/mode/xml"
|
|
};
|
|
|
|
const DEFAULT_XML = `<templates>
|
|
</templates>`;
|
|
|
|
// Memoize loadFile so that samples aren't reloaded whenever a sample is selected
|
|
const fileCache = {};
|
|
const loadFile = (path) => {
|
|
if (!(path in fileCache)) {
|
|
fileCache[path] = _loadFile(path);
|
|
}
|
|
return fileCache[path];
|
|
}
|
|
|
|
/**
|
|
* Make an iframe, with all the js, css and xml properly injected.
|
|
*/
|
|
function makeCodeIframe(js, css, xml) {
|
|
// escape backticks in the xml so they don't close the template string
|
|
const escapedXml = xml.replace(/`/g, '\\\`');
|
|
|
|
const iframe = document.createElement("iframe");
|
|
iframe.onload = () => {
|
|
const doc = iframe.contentDocument;
|
|
|
|
const importMap = doc.createElement("script");
|
|
importMap.type = "importmap";
|
|
importMap.textContent = `{ "imports": { "@odoo/owl": "../owl.js" } }`;
|
|
doc.head.appendChild(importMap);
|
|
|
|
const script = doc.createElement("script");
|
|
script.type = "module";
|
|
script.textContent = `const TEMPLATES = \`${escapedXml}\`\n${js}`;
|
|
doc.body.appendChild(script);
|
|
|
|
const style = document.createElement("style");
|
|
style.innerText = css;
|
|
doc.head.appendChild(style);
|
|
};
|
|
return iframe;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// SAMPLES
|
|
//------------------------------------------------------------------------------
|
|
|
|
const SAMPLES = [
|
|
{
|
|
description: "Components",
|
|
folder: "components",
|
|
code: ["js", "xml", "css"],
|
|
},
|
|
{
|
|
description: "Form Input Bindings",
|
|
folder: "form",
|
|
code: ["js", "xml"],
|
|
},
|
|
{
|
|
description: "Inline templates",
|
|
folder: "single_file_component",
|
|
code: ["js"],
|
|
},
|
|
{
|
|
description: "Lifecycle demo",
|
|
folder: "lifecycle_demo",
|
|
code: ["js", "xml", "css"],
|
|
},
|
|
{
|
|
description: "Customized hook",
|
|
folder: "custom_hooks",
|
|
code: ["js", "xml", "css"],
|
|
},
|
|
{
|
|
description: "Todo List App (with reactivity)",
|
|
folder: "todo_app",
|
|
code: ["js", "xml", "css"],
|
|
},
|
|
{
|
|
description: "Responsive app",
|
|
folder: "responsive_app",
|
|
code: ["js", "xml", "css"],
|
|
},
|
|
{
|
|
description: "Slots And Generic Components",
|
|
folder: "slots",
|
|
code: ["js", "xml", "css"],
|
|
},
|
|
{
|
|
description: "Window Management System",
|
|
folder: "window_manager",
|
|
code: ["js", "xml", "css"],
|
|
},
|
|
{
|
|
description: "Benchmark example",
|
|
folder: "benchmark",
|
|
code: ["js", "xml", "css"],
|
|
},
|
|
]
|
|
|
|
function loadSamples() {
|
|
const result = SAMPLES.map(({ description, folder, code }) => ({
|
|
description,
|
|
code: async () => Object.fromEntries(
|
|
await Promise.all(
|
|
code.map(async (type) => [type, await loadFile(`./samples/${folder}/${folder}.${type}`)])
|
|
)
|
|
),
|
|
}));
|
|
const localSample = localStorage.getItem("owl-playground-local-sample");
|
|
if (localSample) {
|
|
const { js, css, xml } = JSON.parse(localSample);
|
|
result.unshift({
|
|
description: "Local Storage Code",
|
|
code: () => Promise.resolve({ js, xml, css }),
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Tabbed editor
|
|
//------------------------------------------------------------------------------
|
|
class TabbedEditor extends Component {
|
|
setup() {
|
|
const props = this.props;
|
|
this.state = useState({
|
|
currentTab: props.js !== false ? "js" : props.xml ? "xml" : "css"
|
|
});
|
|
this.setTab = debounce(this.setTab.bind(this), 250, true);
|
|
|
|
this.sessions = {};
|
|
this._setupSessions(props);
|
|
this.editorNode = useRef("editor");
|
|
this._updateCode = this._updateCode.bind(this);
|
|
|
|
onMounted(() => {
|
|
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);
|
|
});
|
|
|
|
onPatched(() => {
|
|
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();
|
|
}
|
|
});
|
|
|
|
onWillUpdateProps((nextProps) => this._setupSessions(nextProps));
|
|
|
|
onWillUnmount(() => {
|
|
clearInterval(this.interval);
|
|
this.editor.off("blur", this._updateCode);
|
|
});
|
|
}
|
|
|
|
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.props.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.props.updateCode({
|
|
type: this.state.currentTab,
|
|
value: editorValue
|
|
});
|
|
}
|
|
}
|
|
}
|
|
TabbedEditor.template = "TabbedEditor";
|
|
|
|
//------------------------------------------------------------------------------
|
|
// MAIN APP
|
|
//------------------------------------------------------------------------------
|
|
class Playground extends Component {
|
|
static template = "Playground";
|
|
static components = { TabbedEditor };
|
|
setup() {
|
|
this.version = __info__.version;
|
|
|
|
this.isDirty = false;
|
|
this.state = useState({
|
|
js: "",
|
|
css: "",
|
|
xml: DEFAULT_XML,
|
|
displayWelcome: true,
|
|
splitLayout: true,
|
|
leftPaneWidth: Math.ceil(window.innerWidth / 2),
|
|
topPanelHeight: null
|
|
});
|
|
|
|
this.samples = loadSamples();
|
|
if (window.location.hash) {
|
|
try {
|
|
const { js, css, xml } = JSON.parse(atob(decodeURIComponent(window.location.hash.slice(1))));
|
|
if ([js, css, xml].every(item => typeof item === "string")) {
|
|
Object.assign(this.state, { js, css, xml });
|
|
}
|
|
} catch {}
|
|
}
|
|
onWillStart(async () => {
|
|
if (!this.state.js) {
|
|
this.setSample(await this.samples[0].code());
|
|
}
|
|
})
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
if (this.isDirty) {
|
|
const { js, css, xml } = this.state;
|
|
const str = JSON.stringify({ js, css, xml });
|
|
localStorage.setItem("owl-playground-local-sample", str);
|
|
}
|
|
}, 1000);
|
|
return () => clearInterval(interval);
|
|
}, () => []);
|
|
|
|
this.toggleLayout = debounce(this.toggleLayout, 250, true);
|
|
this.runCode = debounce(this.runCode, 250, true);
|
|
this.exportStandaloneApp = debounce(this.exportStandaloneApp, 250, true);
|
|
this.content = useRef("content");
|
|
this.updateCode = this.updateCode.bind(this);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
shareCode() {
|
|
const state = btoa(JSON.stringify({ js: this.state.js, css: this.state.css, xml: this.state.xml }));
|
|
const link = new URL(window.location.href);
|
|
link.hash = state;
|
|
if (navigator.clipboard) {
|
|
navigator.clipboard.writeText(link.href);
|
|
clearTimeout(this.state.copied)
|
|
this.state.copied = setTimeout(() => this.state.copied = null, 2000);
|
|
}
|
|
window.location.href = link.href;
|
|
}
|
|
|
|
setSample(sample) {
|
|
this.state.js = sample.js;
|
|
this.state.css = sample.css || "";
|
|
this.state.xml = sample.xml || DEFAULT_XML;
|
|
localStorage.removeItem("owl-playground-local-sample");
|
|
this.isDirty = false;
|
|
}
|
|
|
|
get leftPaneStyle() {
|
|
return `width:${this.state.leftPaneWidth}px`;
|
|
}
|
|
|
|
get topEditorStyle() {
|
|
return `flex: 0 0 ${this.state.topPanelHeight}px`;
|
|
}
|
|
|
|
async onSampleChange(ev) {
|
|
this.setSample(await this.samples.find(s => s.description === ev.target.value).code());
|
|
}
|
|
|
|
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({type, value}) {
|
|
if (this.state[type] !== value) {
|
|
this.state[type] = value;
|
|
this.isDirty = true;
|
|
}
|
|
}
|
|
toggleLayout() {
|
|
this.state.splitLayout = !this.state.splitLayout;
|
|
}
|
|
updatePanelHeight({ delta }) {
|
|
let height = this.state.topPanelHeight || document.querySelector(".tabbed-editor").clientHeight;
|
|
this.state.topPanelHeight = height + delta;
|
|
}
|
|
|
|
async exportStandaloneApp() {
|
|
const { js, css, xml } = this.state;
|
|
|
|
await loadJS("libs/jszip.min.js");
|
|
const zip = new JSZip();
|
|
zip.file("app.py", await loadFile("./standalone_app/app.py"));
|
|
zip.file("index.html", await loadFile("./standalone_app/index.html"));
|
|
zip.file("owl.js", await loadFile("../owl.js"));
|
|
zip.file("app.js", `const TEMPLATES = await (await fetch('app.xml')).text();\n${js}`);
|
|
zip.file("app.css", css);
|
|
zip.file("app.xml", xml);
|
|
|
|
await loadJS("libs/FileSaver.min.js");
|
|
saveAs(await zip.generateAsync({ type: "blob" }), "app.zip");
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Application initialization
|
|
//------------------------------------------------------------------------------
|
|
async function start() {
|
|
document.title = `${document.title} (v${__info__.version})`;
|
|
const commit = `https://github.com/odoo/owl/commit/${__info__.hash}`;
|
|
console.info(`This application is using Owl built with the following commit:`, commit);
|
|
const [templates] = await Promise.all([
|
|
loadFile("templates.xml"),
|
|
whenReady()
|
|
]);
|
|
const rootApp = new App(Playground);
|
|
rootApp.addTemplates(templates);
|
|
|
|
await rootApp.mount(document.body);
|
|
}
|
|
|
|
start();
|