From 9475de4d188f5df4e62cbe36129f0e0470f6d02b Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Fri, 21 Apr 2023 07:51:00 +0200 Subject: [PATCH] [IMP/FIX] playground: fix and improve various things 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. --- tools/playground/export_snippet.js | 123 -- tools/playground/index.html | 12 +- tools/playground/playground.css | 2 +- tools/playground/{app.js => playground.js} | 264 ++- tools/playground/samples.js | 1818 ----------------- .../samples/benchmark/benchmark.css | 11 + .../playground/samples/benchmark/benchmark.js | 124 ++ .../samples/benchmark/benchmark.xml | 52 + .../samples/components/components.css | 10 + .../samples/components/components.js | 26 + .../samples/components/components.xml | 9 + .../samples/custom_hooks/custom_hooks.css | 6 + .../samples/custom_hooks/custom_hooks.js | 40 + .../samples/custom_hooks/custom_hooks.xml | 7 + tools/playground/samples/form/form.js | 22 + tools/playground/samples/form/form.xml | 37 + tools/playground/samples/jsconfig.json | 7 + .../samples/lifecycle_demo/lifecycle_demo.css | 11 + .../samples/lifecycle_demo/lifecycle_demo.js | 68 + .../samples/lifecycle_demo/lifecycle_demo.xml | 15 + .../samples/responsive_app/responsive_app.css | 56 + .../samples/responsive_app/responsive_app.js | 118 ++ .../samples/responsive_app/responsive_app.xml | 42 + .../single_file_component.js | 28 + tools/playground/samples/slots/slots.css | 43 + tools/playground/samples/slots/slots.js | 49 + tools/playground/samples/slots/slots.xml | 33 + .../playground/samples/todo_app/todo_app.css | 379 ++++ tools/playground/samples/todo_app/todo_app.js | 182 ++ .../playground/samples/todo_app/todo_app.xml | 50 + .../samples/window_manager/window_manager.css | 70 + .../samples/window_manager/window_manager.js | 192 ++ .../samples/window_manager/window_manager.xml | 32 + tools/playground/standalone_app/app.py | 28 + tools/playground/standalone_app/index.html | 22 + tools/playground/templates.xml | 18 +- tools/playground/utils.js | 6 - tools/server.py | 4 +- 38 files changed, 1946 insertions(+), 2070 deletions(-) delete mode 100644 tools/playground/export_snippet.js rename tools/playground/{app.js => playground.js} (62%) delete mode 100644 tools/playground/samples.js create mode 100644 tools/playground/samples/benchmark/benchmark.css create mode 100644 tools/playground/samples/benchmark/benchmark.js create mode 100644 tools/playground/samples/benchmark/benchmark.xml create mode 100644 tools/playground/samples/components/components.css create mode 100644 tools/playground/samples/components/components.js create mode 100644 tools/playground/samples/components/components.xml create mode 100644 tools/playground/samples/custom_hooks/custom_hooks.css create mode 100644 tools/playground/samples/custom_hooks/custom_hooks.js create mode 100644 tools/playground/samples/custom_hooks/custom_hooks.xml create mode 100644 tools/playground/samples/form/form.js create mode 100644 tools/playground/samples/form/form.xml create mode 100644 tools/playground/samples/jsconfig.json create mode 100644 tools/playground/samples/lifecycle_demo/lifecycle_demo.css create mode 100644 tools/playground/samples/lifecycle_demo/lifecycle_demo.js create mode 100644 tools/playground/samples/lifecycle_demo/lifecycle_demo.xml create mode 100644 tools/playground/samples/responsive_app/responsive_app.css create mode 100644 tools/playground/samples/responsive_app/responsive_app.js create mode 100644 tools/playground/samples/responsive_app/responsive_app.xml create mode 100644 tools/playground/samples/single_file_component/single_file_component.js create mode 100644 tools/playground/samples/slots/slots.css create mode 100644 tools/playground/samples/slots/slots.js create mode 100644 tools/playground/samples/slots/slots.xml create mode 100644 tools/playground/samples/todo_app/todo_app.css create mode 100644 tools/playground/samples/todo_app/todo_app.js create mode 100644 tools/playground/samples/todo_app/todo_app.xml create mode 100644 tools/playground/samples/window_manager/window_manager.css create mode 100644 tools/playground/samples/window_manager/window_manager.js create mode 100644 tools/playground/samples/window_manager/window_manager.xml create mode 100644 tools/playground/standalone_app/app.py create mode 100644 tools/playground/standalone_app/index.html diff --git a/tools/playground/export_snippet.js b/tools/playground/export_snippet.js deleted file mode 100644 index 7e0b8aef..00000000 --- a/tools/playground/export_snippet.js +++ /dev/null @@ -1,123 +0,0 @@ -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 = ` - - - - 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 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" }); -} diff --git a/tools/playground/index.html b/tools/playground/index.html index 6b9ba063..00875d04 100644 --- a/tools/playground/index.html +++ b/tools/playground/index.html @@ -6,16 +6,12 @@ - - - - + + + - - - - + diff --git a/tools/playground/playground.css b/tools/playground/playground.css index 191bf196..907ac7bf 100644 --- a/tools/playground/playground.css +++ b/tools/playground/playground.css @@ -20,7 +20,7 @@ body { display: grid; height: 100%; width: 100%; - grid-template-columns: auto 8px 100%; + grid-template-columns: auto 8px 1fr; } /* LEFT BAR ****************************************/ diff --git a/tools/playground/app.js b/tools/playground/playground.js similarity index 62% rename from tools/playground/app.js rename to tools/playground/playground.js index 8fd5ce6e..068161a4 100644 --- a/tools/playground/app.js +++ b/tools/playground/playground.js @@ -1,7 +1,19 @@ -import { SAMPLES } from "./samples.js"; import { debounce, loadJS } from "./utils.js"; -import { exportStandaloneApp } from "./export_snippet.js"; -const { useState, useRef, onMounted, onWillUnmount, onPatched, onWillUpdateProps } = owl; +import { + App, + Component, + useState, + useRef, + onMounted, + onWillUnmount, + onPatched, + onWillUpdateProps, + loadFile as _loadFile, + whenReady, + __info__, + useEffect, + onWillStart, +} from "../owl.js"; //------------------------------------------------------------------------------ // Constants, helpers, utils @@ -16,40 +28,38 @@ const MODES = { const DEFAULT_XML = ` `; +// 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) { - const sanitizedXML = xml.replace(//g, "").replace(/`/g, '\\\`'); + // escape backticks in the xml so they don't close the template string + const escapedXml = xml.replace(/`/g, '\\\`'); - // create iframe const iframe = document.createElement("iframe"); - iframe.onload = () => { const doc = iframe.contentDocument; - const utilsMod = doc.createElement("script"); - utilsMod.setAttribute("type", "module"); - utilsMod.setAttribute("src", "utils.js"); - doc.head.appendChild(utilsMod); - // 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 = "module"; - const content = ` - (async function(TEMPLATES) { - ${js} - })(\`${sanitizedXML}\`)`; - script.innerHTML = content; - doc.body.appendChild(script); - }); - doc.head.appendChild(owlScript); - // inject css + 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.innerHTML = css; + style.innerText = css; doc.head.appendChild(style); }; return iframe; @@ -58,53 +68,84 @@ function makeCodeIframe(js, css, xml) { //------------------------------------------------------------------------------ // 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() { - let result = SAMPLES.slice(); + 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: js, - xml, - css + code: () => Promise.resolve({ 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.useComponent(); - 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 { +class TabbedEditor extends Component { setup() { const props = this.props; this.state = useState({ @@ -168,7 +209,7 @@ class TabbedEditor extends owl.Component { const resizer = ev => { const delta = ev.clientY - y; y = ev.clientY; - this.trigger("updatePanelHeight", { delta }); + this.props.updatePanelHeight({ delta }); }; document.body.addEventListener("mousemove", resizer); document.body.addEventListener("mouseup", () => { @@ -205,36 +246,53 @@ TabbedEditor.template = "TabbedEditor"; //------------------------------------------------------------------------------ // MAIN APP //------------------------------------------------------------------------------ -class App extends owl.Component { +class Playground extends Component { + static template = "Playground"; + static components = { TabbedEditor }; setup() { - this.version = owl.__info__.version; - this.SAMPLES = useSamples(); - this.isDirty = false; + this.version = __info__.version; + this.isDirty = false; this.state = useState({ - js: this.SAMPLES[0].code, - css: this.SAMPLES[0].css || "", - xml: this.SAMPLES[0].xml || DEFAULT_XML, + js: "", + css: "", + xml: DEFAULT_XML, displayWelcome: true, splitLayout: true, leftPaneWidth: Math.ceil(window.innerWidth / 2), topPanelHeight: null }); - this.toggleLayout = debounce(this.toggleLayout, 250, true); - this.runCode = debounce(this.runCode, 250, true); - this.downloadCode = debounce(this.downloadCode, 250, true); - this.content = useRef("content"); - this.updateCode = this.updateCode.bind(this); + 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")) { - return; + if ([js, css, xml].every(item => typeof item === "string")) { + Object.assign(this.state, { js, css, xml }); } - 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() { @@ -258,12 +316,11 @@ class App extends owl.Component { window.location.href = link.href; } - setSample(ev) { - const sample = this.SAMPLES.find(s => s.description === ev.target.value); - this.state.js = sample.code; + setSample(sample) { + this.state.js = sample.js; this.state.css = sample.css || ""; this.state.xml = sample.xml || DEFAULT_XML; - deleteLocalSample(); + localStorage.removeItem("owl-playground-local-sample"); this.isDirty = false; } @@ -271,14 +328,14 @@ class App extends owl.Component { 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`; } + 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; @@ -305,39 +362,40 @@ class App extends owl.Component { 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; + updatePanelHeight({ delta }) { + let height = this.state.topPanelHeight || document.querySelector(".tabbed-editor").clientHeight; + this.state.topPanelHeight = height + delta; } - async downloadCode() { + async exportStandaloneApp() { const { js, css, xml } = this.state; - const content = await exportStandaloneApp(js, css, xml); + + 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(content, "app.zip"); + saveAs(await zip.generateAsync({ type: "blob" }), "app.zip"); } } -App.components = { TabbedEditor }; -App.template = "App"; //------------------------------------------------------------------------------ // Application initialization //------------------------------------------------------------------------------ async function start() { - document.title = `${document.title} (v${owl.__info__.version})`; - const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`; + 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([ - owl.loadFile("templates.xml"), - owl.whenReady() + loadFile("templates.xml"), + whenReady() ]); - const rootApp = new owl.App(App); + const rootApp = new App(Playground); rootApp.addTemplates(templates); await rootApp.mount(document.body); diff --git a/tools/playground/samples.js b/tools/playground/samples.js deleted file mode 100644 index 8299d673..00000000 --- a/tools/playground/samples.js +++ /dev/null @@ -1,1818 +0,0 @@ -const COMPONENTS = /*js*/ ` -// In this example, we show how components can be defined and created. -const { Component, useState, mount } = owl; - -class Greeter extends Component { - static template = "Greeter"; - - setup() { - this.state = useState({ word: 'Hello' }); - } - - toggle() { - this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'; - } -} - -// Main root component -class Root extends Component { - static components = { Greeter }; - static template = "Root" - - setup() { - this.state = useState({ name: 'World'}); - } -} - -mount(Root, document.body, { templates: TEMPLATES, dev: true });`; - -const COMPONENTS_XML = /*xml*/` - -
- , -
- - - - -
-`; - -const COMPONENTS_CSS = /*css*/` -.greeter { - font-size: 20px; - width: 300px; - height: 100px; - margin: 5px; - text-align: center; - line-height: 100px; - background-color: #eeeeee; - user-select: none; -}`; - -const LIFECYCLE_DEMO = /*js*/` -// This example shows all the possible lifecycle hooks -// -// The root component controls a sub component (DemoComponent). It logs all its lifecycle -// methods in the console. Try modifying its state by clicking on it, or by -// clicking on the two main buttons, and look into the console to see what -// happens. -const { Component, useState, mount, useComponent, onWillStart, onMounted, onWillUnmount, onWillUpdateProps, onPatched, onWillPatch, onWillRender, onRendered, onWillDestroy} = owl; - -function useLogLifecycle() { - const component = useComponent(); - const name = component.constructor.name; - onWillStart(() => console.log(\`\${name}:willStart\`)); - onMounted(() => console.log(\`\${name}:mounted\`)); - onWillUpdateProps(() => console.log(\`\${name}:willUpdateProps\`)); - onWillRender(() => console.log(\`\${name}:willRender\`)); - onRendered(() => console.log(\`\${name}:rendered\`)); - onWillPatch(() => console.log(\`\${name}:willPatch\`)); - onPatched(() => console.log(\`\${name}:patched\`)); - onWillUnmount(() => console.log(\`\${name}:willUnmount\`)); - onWillDestroy(() => console.log(\`\${name}:willDestroy\`)); -} - -class DemoComponent extends Component { - static template = "DemoComponent"; - - setup() { - useLogLifecycle(); - this.state = useState({ n: 0 }); - } - increment() { - this.state.n++; - } -} - -class Root extends Component { - static template = "Root"; - static components = { DemoComponent }; - - setup() { - useLogLifecycle(); - this.state = useState({ n: 0, flag: true }); - } - - increment() { - this.state.n++; - } - - toggleSubComponent() { - this.state.flag = !this.state.flag; - } -} - -mount(Root, document.body, { templates: TEMPLATES, dev: true }); -`; - -const LIFECYCLE_DEMO_XML = /*xml*/ ` - -
-
Demo Sub Component
-
(click on me to update me)
-
Props: , State: .
-
- -
- - -
- -
-
-
`; - -const LIFECYCLE_CSS = /*css*/` -button { - font-size: 18px; - margin: 5px; -} - -.demo { - margin: 10px; - padding: 10px; - background-color: #dddddd; - width: 250px; -}`; - -const HOOKS_DEMO = /*js*/ ` -// In this example, we show how hooks can be used or defined. -const { Component, mount, useState, onWillDestroy } = owl; - -// We define here a custom behaviour: this hook tracks the state of the mouse -// position -function useMouse() { - const position = useState({x:0, y: 0}); - - function update(e) { - position.x = e.clientX; - position.y = e.clientY; - } - window.addEventListener('mousemove', update); - onWillDestroy(() => { - window.removeEventListener('mousemove', update); - }); - - return position; -} - - -// Main root component -class Root extends Component { - static template = "Root"; - - setup() { - // simple state hook (reactive object) - this.counter = useState({ value: 0 }); - - // this hooks is bound to the 'mouse' property. - this.mouse = useMouse(); - } - - increment() { - this.counter.value++; - } -} - -// Application setup -mount(Root, document.body, { templates: TEMPLATES, dev: true }); -`; - -const HOOKS_DEMO_XML = /*xml*/ ` - -
- -
Mouse: ,
-
-
-`; - -const HOOKS_CSS = /*css*/ `button { - width: 120px; - height: 35px; - font-size: 16px; -}`; - -const TODO_APP_REACTIVITY = /*js*/ ` - -// This example is an implementation of the TodoList application, from the -// www.todomvc.com project. This is a non trivial application with some -// interesting user interactions. It uses the local storage for persistence. -// -// In this implementation, we use the owl reactivity mechanism. -const { Component, useState, mount, useRef, onPatched, onMounted, reactive, useEnv, useEffect } = owl; - -//------------------------------------------------------------------------------ -// Constants, helpers -//------------------------------------------------------------------------------ -const ENTER_KEY = 13; -const ESC_KEY = 27; -const LOCALSTORAGE_KEY = "todomvc"; - -function useAutofocus(name) { - let ref = useRef(name); - useEffect(el => el && el.focus(), () => [ref.el]); -} - -function useStore() { - const env = useEnv(); - return useState(env.store); -} - -//------------------------------------------------------------------------------ -// Task store -//------------------------------------------------------------------------------ -class TaskList { - constructor(tasks) { - this.tasks = tasks || []; - const taskIds = this.tasks.map((t) => t.id); - this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1; - } - - addTask(text) { - text = text.trim(); - if (text) { - const task = { - id: this.nextId++, - text: text, - isCompleted: false, - }; - this.tasks.push(task); - } - } - - toggleTask(task) { - task.isCompleted = !task.isCompleted; - } - - toggleTask(id) { - const task = this.tasks.find(t => t.id === id); - task.isCompleted = !task.isCompleted; - } - - toggleAll(value) { - for (let task of this.tasks) { - task.isCompleted = value; - } - } - - clearCompleted() { - const tasks = this.tasks.filter(t => t.isCompleted); - for (let task of tasks) { - this.deleteTask(task); - } - } - - deleteTask(id) { - const index = this.tasks.findIndex((t) => t.id === id); - this.tasks.splice(index, 1); - } - - updateTask(id, text) { - const value = text.trim(); - if (!value) { - this.deleteTask(id); - } else { - const task = this.tasks.find(t => t.id === id); - task.text = value; - } - } -} - -function createTaskStore() { - const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks)); - const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]"); - const taskStore = reactive(new TaskList(initialTasks), saveTasks); - saveTasks(); - return taskStore; -} - -//------------------------------------------------------------------------------ -// Todo -//------------------------------------------------------------------------------ -class Todo extends Component { - static template = "Todo"; - - setup() { - useAutofocus("input"); - this.store = useStore(); - this.state = useState({ - isEditing: false - }); - } - - handleKeyup(ev) { - if (ev.keyCode === ENTER_KEY) { - this.updateText(ev.target.value); - } - if (ev.keyCode === ESC_KEY) { - ev.target.value = this.props.text; - this.state.isEditing = false; - } - } - - handleBlur(ev) { - this.updateText(ev.target.value); - } - - updateText(text) { - this.store.updateTask(this.props.id, text); - this.state.isEditing = false; - } -} - -//------------------------------------------------------------------------------ -// TodoList -//------------------------------------------------------------------------------ -class TodoList extends Component { - static template = "TodoList"; - static components = { Todo }; - - setup() { - this.store = useStore(); - this.state = useState({ filter: "all" }); - } - - get displayedTasks() { - const tasks = this.store.tasks; - switch (this.state.filter) { - case "active": - return tasks.filter((t) => !t.isCompleted); - case "completed": - return tasks.filter((t) => t.isCompleted); - case "all": - return tasks; - } - } - - get allChecked() { - return this.store.tasks.every(todo => todo.isCompleted); - } - - get remaining() { - return this.store.tasks.filter(todo => !todo.isCompleted).length; - } - - get remainingText() { - const items = this.remaining < 2 ? "item" : "items"; - return \` \${items} left\`; - } - - addTodo(ev) { - if (ev.keyCode === ENTER_KEY) { - const text = ev.target.value; - if (text.trim()) { - this.store.addTask(text); - } - ev.target.value = ""; - } - } - - setFilter(filter) { - this.state.filter = filter; - } -} - -//------------------------------------------------------------------------------ -// App Initialization -//------------------------------------------------------------------------------ -const env = { store: createTaskStore() }; -mount(TodoList, document.body, { env, templates: TEMPLATES, dev: true }); -`; - -const TODO_APP_REACTIVITY_XML = /*xml*/ ` - -
-
-

todos

- -
-
- - -
    - - - -
-
- -
- -
  • -
    - - - -
    - -
  • -
    `; - -const TODO_APP_REACTIVITY_CSS = /*css*/` -html,body { - margin: 0; - padding: 0; -} - -button { - margin: 0; - padding: 0; - border: 0; - background: none; - font-size: 100%; - vertical-align: baseline; - font-family: inherit; - font-weight: inherit; - color: inherit; - -webkit-appearance: none; - appearance: none; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif; - line-height: 1.4em; - background: #f5f5f5; - color: #4d4d4d; - min-width: 230px; - max-width: 550px; - margin: 0 auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - font-weight: 300; -} - -:focus { - outline: 0; -} - -.hidden { - display: none; -} - -.todoapp { - background: #fff; - margin: 130px 0 40px 0; - position: relative; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); -} - -.todoapp input::-webkit-input-placeholder { - font-style: italic; - font-weight: 300; - color: #e6e6e6; -} - -.todoapp input::-moz-placeholder { - font-style: italic; - font-weight: 300; - color: #e6e6e6; -} - -.todoapp input::input-placeholder { - font-style: italic; - font-weight: 300; - color: #e6e6e6; -} - -.todoapp h1 { - position: absolute; - top: -155px; - width: 100%; - font-size: 100px; - font-weight: 100; - text-align: center; - color: rgba(175, 47, 47, 0.15); - -webkit-text-rendering: optimizeLegibility; - -moz-text-rendering: optimizeLegibility; - text-rendering: optimizeLegibility; -} - -.new-todo, -.edit { - position: relative; - margin: 0; - width: 100%; - font-size: 24px; - font-family: inherit; - font-weight: inherit; - line-height: 1.4em; - border: 0; - color: inherit; - padding: 6px; - border: 1px solid #999; - box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); - box-sizing: border-box; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.new-todo { - padding: 16px 16px 16px 60px; - border: none; - background: rgba(0, 0, 0, 0.003); - box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); -} - -.main { - position: relative; - z-index: 2; - border-top: 1px solid #e6e6e6; -} - -.toggle-all { - width: 1px; - height: 1px; - border: none; /* Mobile Safari */ - opacity: 0; - position: absolute; - right: 100%; - bottom: 100%; -} - -.toggle-all + label { - width: 60px; - height: 34px; - font-size: 0; - position: absolute; - top: -52px; - left: -13px; - -webkit-transform: rotate(90deg); - transform: rotate(90deg); -} - -.toggle-all + label:before { - content: "❯"; - font-size: 22px; - color: #e6e6e6; - padding: 10px 27px 10px 27px; -} - -.toggle-all:checked + label:before { - color: #737373; -} - -.todo-list { - margin: 0; - padding: 0; - list-style: none; -} - -.todo-list li { - position: relative; - font-size: 24px; - border-bottom: 1px solid #ededed; -} - -.todo-list li:last-child { - border-bottom: none; -} - -.todo-list li.editing { - border-bottom: none; - padding: 0; -} - -.todo-list li.editing .edit { - display: block; - width: calc(100% - 43px); - padding: 12px 16px; - margin: 0 0 0 43px; -} - -.todo-list li.editing .view { - display: none; -} - -.todo-list li .toggle { - text-align: center; - width: 40px; - /* auto, since non-WebKit browsers doesn't support input styling */ - height: auto; - position: absolute; - top: 0; - bottom: 0; - margin: auto 0; - border: none; /* Mobile Safari */ - -webkit-appearance: none; - appearance: none; -} - -.todo-list li .toggle { - opacity: 0; -} - -.todo-list li .toggle + label { - /* - Firefox requires \`#\` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 - IE and Edge requires *everything* to be escaped to render, so we do that instead of just the \`#\` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ - */ - background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: center left; -} - -.todo-list li .toggle:checked + label { - background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E"); -} - -.todo-list li label { - word-break: break-all; - padding: 15px 15px 15px 60px; - display: block; - line-height: 1.2; - transition: color 0.4s; -} - -.todo-list li.completed label { - color: #d9d9d9; - text-decoration: line-through; -} - -.todo-list li .destroy { - display: none; - position: absolute; - top: 0; - right: 10px; - bottom: 0; - width: 40px; - height: 40px; - margin: auto 0; - font-size: 30px; - color: #cc9a9a; - margin-bottom: 11px; - transition: color 0.2s ease-out; -} - -.todo-list li .destroy:hover { - color: #af5b5e; -} - -.todo-list li .destroy:after { - content: "×"; -} - -.todo-list li:hover .destroy { - display: block; -} - -.todo-list li .edit { - display: none; -} - -.todo-list li.editing:last-child { - margin-bottom: -1px; -} - -.footer { - color: #777; - padding: 10px 15px; - height: 20px; - text-align: center; - border-top: 1px solid #e6e6e6; -} - -.footer:before { - content: ""; - position: absolute; - right: 0; - bottom: 0; - left: 0; - height: 50px; - overflow: hidden; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, - 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, - 0 17px 2px -6px rgba(0, 0, 0, 0.2); -} - -.todo-count { - float: left; - text-align: left; -} - -.todo-count strong { - font-weight: 300; -} - -.filters { - margin: 0; - padding: 0; - list-style: none; - position: absolute; - right: 0; - left: 0; -} - -.filters li { - display: inline; -} - -.filters li a { - color: inherit; - margin: 3px; - padding: 3px 7px; - text-decoration: none; - border: 1px solid transparent; - border-radius: 3px; -} - -.filters li a:hover { - border-color: rgba(175, 47, 47, 0.1); -} - -.filters li a.selected { - border-color: rgba(175, 47, 47, 0.2); -} - -.clear-completed, -html .clear-completed:active { - float: right; - position: relative; - line-height: 20px; - text-decoration: none; - cursor: pointer; -} - -.clear-completed:hover { - text-decoration: underline; -} - -.info { - margin: 65px auto 0; - color: #bfbfbf; - font-size: 10px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - text-align: center; -} - -.info p { - line-height: 1; -} - -.info a { - color: inherit; - text-decoration: none; - font-weight: 400; -} - -.info a:hover { - text-decoration: underline; -} - -/* - Hack to remove background from Mobile Safari. - Can't use it globally since it destroys checkboxes in Firefox -*/ -@media screen and (-webkit-min-device-pixel-ratio: 0) { - .toggle-all, - .todo-list li .toggle { - background: none; - } - - .todo-list li .toggle { - height: 40px; - } -} - -@media (max-width: 430px) { - .footer { - height: 50px; - } - - .filters { - bottom: 10px; - } -} - -.filters a { - cursor: pointer; -} -`; - -const RESPONSIVE = /*js*/ ` -// In this example, we show how one can design an application that is responsive: -// its UI is different in mobile mode or in desktop mode. -// -// The main idea is to have a "isMobile" key in the environment, then listen -// to resize events and update the env if needed. Then, the whole interface -// will be updated, creating and destroying components as needed. -// -// To see this in action, try resizing the window. The application will switch -// to mobile mode whenever it has less than 768px. -const { Component, useState, mount, reactive, useEnv } = owl; - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -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); - } - }; -} - -//------------------------------------------------------------------------------ -// Responsive hook -//------------------------------------------------------------------------------ - -function createUI() { - const getIsMobile = () => window.innerWidth <= 768; - - const ui = reactive({ isMobile: getIsMobile() }); - - const updateEnv = debounce(() => { - const isMobile = getIsMobile(); - if (ui.isMobile !== isMobile) { - ui.isMobile = isMobile; - } - }, 15); - window.addEventListener("resize", updateEnv); - return ui; -} - -function useUI() { - const env = useEnv(); - return useState(env.ui); -} - - -//------------------------------------------------------------------------------ -// Components -//------------------------------------------------------------------------------ -class Navbar extends owl.Component { - static template = "Navbar"; -} - -class MobileSearchView extends Component { - static template = "MobileSearchView"; -} - -class ControlPanel extends Component { - static template = "ControlPanel"; - static components = { MobileSearchView }; - setup() { - this.ui = useUI(); - } -} - -class AdvancedComponent extends Component { - static template = "AdvancedComponent"; -} - -class FormView extends Component { - static template = "FormView"; - static components = { AdvancedComponent }; - setup() { - this.ui = useUI(); - } -} - -class Chatter extends Component { - static template = "Chatter"; - - setup() { - this.messages = Array.from(Array(100).keys()); - } -} - -class Root extends Component { - static template = "Root"; - static components = { Navbar, ControlPanel, FormView, Chatter }; - - setup() { - this.ui = useUI(); - } -} - - - -//------------------------------------------------------------------------------ -// Application Startup -//------------------------------------------------------------------------------ -const env = { - ui: createUI() -}; - -mount(Root, document.body, { templates: TEMPLATES, env, dev: true }); -`; - -const RESPONSIVE_XML = /*xml*/` - - - -
    -

    Control Panel

    - -
    - -
    -

    Form View

    - -
    - -
    -

    Chatter

    -
    Message
    -
    - -
    Mobile searchview
    - -
    - This component is only created in desktop mode. - -
    - - - - - -
    - - -
    -
    - -
    -
    - - - -
    -
    -`; - -const RESPONSIVE_CSS = /*css*/` -body { - margin: 0; -} - -.app { - height: 100%; - flex-direction: column; -} - -.app.desktop { - display: flex; -} - -.navbar { - flex: 0 0 30px; - height: 30px; - background-color: cadetblue; - color: white; - line-height: 30px; -} - -.controlpanel { - flex: 0 0 100px; - height: 100px; - background-color: #dddddd; - padding: 8px; -} - -.content-wrapper { - flex: 1 1 auto; - position: relative; -} - -.content { - display: flex; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; -} - -.formview { - overflow-y: auto; - flex: 1 1 60%; - min-height: 200px; - padding: 8px; -} - -.chatter { - overflow-y: auto; - flex: 1 1 40%; - background-color: #eeeeee; - color: #333333; - padding: 8px; -} -`; - -const SLOTS = /*js*/ ` - -// We show here how slots can be used to create generic components. -// In this example, the Card component is basically only a container. It is not -// aware of its content. It just knows where it should be (with t-slot). -// The parent component define the content with t-set-slot. -// -// Note that the t-on-click event, defined in the Root template, is executed in -// the context of the Root component, even though it is inside the Card component -const { Component, useState, mount } = owl; - -class Card extends Component { - static template = "Card"; - - setup() { - this.state = useState({ showContent: true }); - } - - toggleDisplay() { - this.state.showContent = !this.state.showContent; - } -} - -class Counter extends Component { - static template = "Counter"; - - setup() { - this.state = useState({val: 1}); - } - - inc() { - this.state.val++; - } -} - -// Main root component -class Root extends Component { - static template = "Root" - static components = { Card, Counter }; - - setup() { - this.state = useState({a: 1, b: 3}); - } - - inc(key, delta) { - this.state[key] += delta; - } -} - -// Application setup -mount(Root, document.body, { templates: TEMPLATES, dev: true}); -`; - -const SLOTS_XML = /*xml*/` - -
    -
    - -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    - - Content of card 1... [] - - - - -
    Card 2... []
    - -
    - -
    -
    -
    -`; - -const SLOTS_CSS = /*css*/ ` -.main { - display: flex; -} - -.card { - display: flex; - flex-direction: column; - background-color: #eeeeee; - width: 200px; - height: 100px; - margin: 10px; - border: 1px solid gray; -} - -.card.full { - height: 100px; -} - -.card.small { - height: 25px; -} - -.card-title { - flex: 0 0 25px; - font-weight: bold; - background-color: darkcyan; - color: white; - padding: 2px; -} - -.card-title button { - float: right; -} - -.card-content { - flex: 1 1 auto; - padding: 5px; - border-top: 1px solid white; -} - -.card-footer { - border-top: 1px solid white; -}`; - -const FORM = /*js*/` -// This example illustrate how the t-model directive can be used to synchronize -// data between html inputs (and select/textareas) and the state of a component. -// Note that there are two controls with t-model="color": they are totally -// synchronized. -const { Component, useState, mount } = owl; - -class Form extends Component { - static template = "Form"; - - setup() { - this.state = useState({ - text: "", - othertext: "", - number: 11, - color: "", - bool: false - }); - } -} - -// Application setup -mount(Form, document.body, { templates: TEMPLATES, dev: true }); -`; - -const FORM_XML = /*xml*/ ` - -
    -

    Form

    -
    - Text (immediate): -
    -
    - Other text (lazy): -
    -
    - Number: -
    -
    - Boolean: -
    -
    - Color, with a select: - -
    -
    - Color, with radio buttons: - - -
    -
    -

    State

    -
    Text:
    -
    Other Text:
    -
    Number:
    -
    Boolean: TrueFalse
    -
    Color:
    -
    -
    -`; - -const WMS = /*js*/` -// This example is slightly more complex than usual. We demonstrate -// here a way to manage sub windows in Owl, declaratively. This is still just a -// demonstration. Managing windows can be as complex as we want. For example, -// we could implement the following features: -// - resizing windows -// - minimizing windows -// - configuration options for windows to make a window non resizeable -// - minimal width/height -// - better heuristic for initial window position -// - ... -const { Component, useState, mount, useRef, reactive, useEnv, onMounted } = owl; - -// ----------------------------------------------------------------------------- -// Window manager code -// ----------------------------------------------------------------------------- - -class WindowManager { - // contains all components with metadata - static Windows = {}; - windows = {}; // mapping id => info - nextId = 1; - - add(type) { - const Comp = WindowManager.Windows[type]; - const left = 50 + Math.round(Math.random()*(window.innerWidth - 50 - Comp.defaultWidth)); - const top = 50 + Math.round(Math.random()*(window.innerHeight - 100 - Comp.defaultHeight)); - const id = this.nextId++; - this.windows[id] = { - id, - title: Comp.defaultTitle, - width: Comp.defaultWidth, - height: Comp.defaultHeight, - left, - top, - Component: Comp, - }; - } - - close(id) { - delete this.windows[id]; - } - - updatePosition(id, left, top) { - const w = this.windows[id]; - w.left = left; - w.top = top; - } - - getWindows() { - return Object.values(this.windows); - } -} - -function createWindowService() { - return reactive(new WindowManager()); -} - -function useWindowService() { - const env = useEnv(); - return useState(env.windowService); -} - -// ----------------------------------------------------------------------------- -// Generic Window Component -// ----------------------------------------------------------------------------- - -class Window extends Component { - static template = "Window"; - static nextZIndex = 1; - zIndex = 0; - - setup() { - this.windowService = useWindowService(); - this.root = useRef('root'); - onMounted(this.updateZIndex); - } - - get style() { - let { width, height, top, left } = this.props.info; - return \`width: $\{width}px;height: $\{height}px;top:$\{top}px;left:$\{left}px;z-index:\${this.zIndex}\`; - } - - close() { - this.windowService.close(this.props.info.id); - } - - startDragAndDrop(ev) { - this.updateZIndex(); - const self = this; - const root = this.root; - - const el = root.el; - el.classList.add('dragging'); - - const current = this.props.info; - const offsetX = current.left - ev.pageX; - const offsetY = current.top - ev.pageY; - let left, top; - - window.addEventListener("mousemove", moveWindow); - window.addEventListener("mouseup", stopDnD, { once: true }); - - function moveWindow(ev) { - left = Math.max(offsetX + ev.pageX, 0); - top = Math.max(offsetY + ev.pageY, 0); - el.style.left = \`\${left}px\`; - el.style.top = \`\${top}px\`; - } - function stopDnD() { - window.removeEventListener("mousemove", moveWindow); - el.classList.remove('dragging'); - - if (top !== undefined && left !== undefined) { - self.windowService.updatePosition(current.id, left, top); - } - } - } - - updateZIndex() { - this.zIndex = Window.nextZIndex++; - this.root.el.style['z-index'] = this.zIndex; - } -} - -// ----------------------------------------------------------------------------- -// Two concrete Window type implementations -// ----------------------------------------------------------------------------- - -class HelloWorld extends Component { - static template = "HelloWorld"; - static defaultTitle = "Hello Owl!"; - static defaultWidth = 200; - static defaultHeight = 100; -} - - -class Counter extends Component { - static template = "Counter"; - static defaultTitle = "Click Counter"; - static defaultWidth = 300; - static defaultHeight = 120; - - state = useState({ value: 0 }); - - inc() { - this.state.value++; - } -} - -// register window components -WindowManager.Windows.Hello = HelloWorld; -WindowManager.Windows.Counter = Counter; - -// ----------------------------------------------------------------------------- -// Window Container -// ----------------------------------------------------------------------------- - -class WindowContainer extends Component { - static template = "WindowContainer"; - static components = { Window }; - - setup() { - this.windowService = useWindowService(); - } -} - -// ----------------------------------------------------------------------------- -// Root Component -// ----------------------------------------------------------------------------- - -class Root extends Component { - static template = "Root"; - static components = { WindowContainer }; - - setup() { - this.windowService = useWindowService(); - } - - addWindow(type) { - this.windowService.add(type); - } -} - -// ----------------------------------------------------------------------------- -// Setup -// ----------------------------------------------------------------------------- - -const env = { - windowService: createWindowService(), -}; - -mount(Root, document.body, { templates: TEMPLATES, env, dev: true }); -`; - -const WMS_XML =/*xml*/` - -
    -
    - - × -
    - -
    - -
    - - - -
    - -
    - - -
    - -
    - Some content here... -
    - -
    - - -
    -
    -`; - -const WMS_CSS= /*css*/` -body { - margin: 0; -} - -.app { - width: 100%; - height: 100%; - display: grid; - grid-template-rows: auto 50px; -} - -.window-manager { - position: relative; - width: 100%; - height: 100%; - background-color: #eeeeee; - overflow: hidden; -} - -.menubar { - background-color: #875a7b; - color: white; -} - -.menubar button { - height: 40px; - font-size: 18px; - margin: 5px; -} - -.window { - display: grid; - grid-template-rows: 30px auto; - border: 1px solid gray; - background-color: white; - position: absolute; - box-shadow: 1px 1px 2px 1px grey; -} - -.window.dragging { - opacity: 0.75; -} - -.window .header { - background-color: #875a7b; - display: grid; - grid-template-columns: auto 24px; - color: white; - line-height: 30px; - padding-left: 5px; - cursor: default; - user-select: none; -} - -.window .header .close { - cursor: pointer; - font-size: 22px; - padding-left: 4px; - padding-right: 4px; - font-weight: bold; -} - -.counter { - font-size: 20px; -} -.counter button { - width: 80px; - height:40px; - font-size: 20px; -}`; - -const SFC =/*js*/` -// This example illustrates how one can write Owl components with -// inline templates. - -const { Component, useState, xml, css, mount } = owl; - -// Counter component -class Counter extends Component { - static template = xml\` - \`; - - state = useState({ value: 0}) -} - -// Root -class Root extends Component { - static template = xml\` -
    - - -
    \`; - - static components = { Counter }; -} - -// Application setup -mount(Root, document.body, { templates: TEMPLATES, dev: true}); -`; - -const BENCHMARK_JS = ` -const { Component, mount, xml, useState, onWillRender, onPatched} = owl; - -// ----------------------------------------------------------------------------- -// Data generation -// ----------------------------------------------------------------------------- - -let idCounter = 1; -const adjectives = [ - "pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain", - "quaint", "clean", "elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd", - "unsightly", "adorable", "important", "inexpensive", "cheap", "expensive", "fancy"]; -const colours = ["red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange"]; -const nouns = ["table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse", "keyboard"]; - -function _random (max) { return Math.round(Math.random() * 1000) % max; }; - -function buildData(count) { - const data = new Array(count); - for (let i = 0; i < count; i++) { - const label = \`\${adjectives[_random(adjectives.length)]} \${colours[_random(colours.length)]} \${nouns[_random(nouns.length)]}\`; - data[i] = { - id: idCounter++, - label, - }; - } - return data; -} - -// ----------------------------------------------------------------------------- -// Components -// ----------------------------------------------------------------------------- -class Button extends Component { - static template = xml\` -
    - -
    \`; -} - - -class Row extends Component { - static template = xml\` - - - - - - - [x] - - - - \` -} - -class Root extends Component { - static template = xml\` -
    -
    -
    -
    -

    Owl Keyed

    -
    -
    -
    -
    -
    -
    -
    - - - - - - -
    -
    \`; - - static components = { Button, Row }; - - setup() { - this.state = useState({ - rows: [], - selectedRowId: null - }); - this.benchmarking = false; - onPatched(() => { - if (this.benchmarking) { - this.stop(); - } - }); - } - - start(descr) { - this.benchmarking = \`[\${descr}]\`; - console.time(this.benchmarking); - } - stop() { - console.timeEnd(this.benchmarking); - this.benchmarking = false; - } - - run() { - this.start('add1000'); - this.state.rows = buildData(1000); - this.state.selectedRowId = null; - } - - runLots() { - this.start('add10_000'); - this.state.rows = buildData(10_000); - this.state.selectedRowId = null; - } - - add() { - this.start('append1000'); - this.state.rows = this.state.rows.concat(buildData(1000)); - } - - update() { - this.start('update1/10th'); - let index = 0; - const rows = this.state.rows; - while (index < rows.length) { - rows[index].label = rows[index].label + " !!!"; - index += 10; - } - } - - clear() { - this.start('clear'); - this.state.rows = []; - this.state.selectedRowId = null; - } - - swapRows() { - this.start('swap'); - const rows = this.state.rows; - if (rows.length > 998) { - let tmp = rows[1]; - rows[1] = rows[998]; - rows[998] = tmp; - } - } - - selectRow(id) { - this.start('select'); - this.state.selectedRowId = id; - } - - removeRow(id) { - this.start('remove1'); - const rows = this.state.rows; - rows.splice(rows.findIndex(row => row.id === id), 1); - } -} - - -// dev=false for benchmarking. we don't want to benchmark dev code! -mount(Root, document.body, { templates: TEMPLATES, dev: false }); - -`; - -const BENCHMARK_CSS = ` -tr.danger { - font-weight: bold; -} - -.remove:hover { - font-weight: bold; -} -.remove { - cursor: pointer; -} -`; - -export const SAMPLES = [ - { - description: "Components", - code: COMPONENTS, - xml: COMPONENTS_XML, - css: COMPONENTS_CSS - }, - { - description: "Form Input Bindings", - code: FORM, - xml: FORM_XML - }, - { - description: "Inline templates", - code: SFC - }, - { - description: "Lifecycle demo", - code: LIFECYCLE_DEMO, - xml: LIFECYCLE_DEMO_XML, - css: LIFECYCLE_CSS - }, - { - description: "Customized hook", - code: HOOKS_DEMO, - xml: HOOKS_DEMO_XML, - css: HOOKS_CSS - }, - { - description: "Todo List App (with reactivity)", - code: TODO_APP_REACTIVITY, - css: TODO_APP_REACTIVITY_CSS, - xml: TODO_APP_REACTIVITY_XML - }, - { - description: "Responsive app", - code: RESPONSIVE, - css: RESPONSIVE_CSS, - xml: RESPONSIVE_XML - }, - { - description: "Slots And Generic Components", - code: SLOTS, - xml: SLOTS_XML, - css: SLOTS_CSS - }, - { - description: "Window Management System", - code: WMS, - xml: WMS_XML, - css: WMS_CSS, - }, - { - description: "Benchmark example", - code: BENCHMARK_JS, - css: BENCHMARK_CSS, - }, -]; diff --git a/tools/playground/samples/benchmark/benchmark.css b/tools/playground/samples/benchmark/benchmark.css new file mode 100644 index 00000000..67ba2ef7 --- /dev/null +++ b/tools/playground/samples/benchmark/benchmark.css @@ -0,0 +1,11 @@ +tr.danger { + font-weight: bold; +} + +.remove:hover { + font-weight: bold; +} +.remove { + cursor: pointer; +} + \ No newline at end of file diff --git a/tools/playground/samples/benchmark/benchmark.js b/tools/playground/samples/benchmark/benchmark.js new file mode 100644 index 00000000..a09689d8 --- /dev/null +++ b/tools/playground/samples/benchmark/benchmark.js @@ -0,0 +1,124 @@ +import { Component, mount, useState, onPatched} from "@odoo/owl"; + +// ----------------------------------------------------------------------------- +// Data generation +// ----------------------------------------------------------------------------- + +let idCounter = 1; +const adjectives = [ + "pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain", + "quaint", "clean", "elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd", + "unsightly", "adorable", "important", "inexpensive", "cheap", "expensive", "fancy"]; +const colours = ["red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange"]; +const nouns = ["table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse", "keyboard"]; + +function _random (max) { return Math.round(Math.random() * 1000) % max; }; + +function buildData(count) { + const data = new Array(count); + for (let i = 0; i < count; i++) { + const label = `${adjectives[_random(adjectives.length)]} ${colours[_random(colours.length)]} ${nouns[_random(nouns.length)]}`; + data[i] = { + id: idCounter++, + label, + }; + } + return data; +} + +// ----------------------------------------------------------------------------- +// Components +// ----------------------------------------------------------------------------- +class Button extends Component { + static template = "Button"; +} + + +class Row extends Component { + static template = "Row"; +} + +class Root extends Component { + static template = "Root"; + static components = { Button, Row }; + + setup() { + this.state = useState({ + rows: [], + selectedRowId: null + }); + this.benchmarking = false; + onPatched(() => { + if (this.benchmarking) { + this.stop(); + } + }); + } + + start(descr) { + this.benchmarking = `[${descr}]`; + console.time(this.benchmarking); + } + stop() { + console.timeEnd(this.benchmarking); + this.benchmarking = false; + } + + run() { + this.start('add1000'); + this.state.rows = buildData(1000); + this.state.selectedRowId = null; + } + + runLots() { + this.start('add10_000'); + this.state.rows = buildData(10_000); + this.state.selectedRowId = null; + } + + add() { + this.start('append1000'); + this.state.rows = this.state.rows.concat(buildData(1000)); + } + + update() { + this.start('update1/10th'); + let index = 0; + const rows = this.state.rows; + while (index < rows.length) { + rows[index].label = rows[index].label + " !!!"; + index += 10; + } + } + + clear() { + this.start('clear'); + this.state.rows = []; + this.state.selectedRowId = null; + } + + swapRows() { + this.start('swap'); + const rows = this.state.rows; + if (rows.length > 998) { + let tmp = rows[1]; + rows[1] = rows[998]; + rows[998] = tmp; + } + } + + selectRow(id) { + this.start('select'); + this.state.selectedRowId = id; + } + + removeRow(id) { + this.start('remove1'); + const rows = this.state.rows; + rows.splice(rows.findIndex(row => row.id === id), 1); + } +} + + +// dev=false for benchmarking. we don't want to benchmark dev code! +mount(Root, document.body, { templates: TEMPLATES, dev: false }); diff --git a/tools/playground/samples/benchmark/benchmark.xml b/tools/playground/samples/benchmark/benchmark.xml new file mode 100644 index 00000000..58c19c42 --- /dev/null +++ b/tools/playground/samples/benchmark/benchmark.xml @@ -0,0 +1,52 @@ + + +
    + +
    +
    + + + + + + + + [x] + + + + + + +
    +
    +
    +
    +

    Owl Keyed

    +
    +
    +
    +
    +
    +
    +
    + + + + + + +
    +
    +
    +
    diff --git a/tools/playground/samples/components/components.css b/tools/playground/samples/components/components.css new file mode 100644 index 00000000..70eee24d --- /dev/null +++ b/tools/playground/samples/components/components.css @@ -0,0 +1,10 @@ +.greeter { + font-size: 20px; + width: 300px; + height: 100px; + margin: 5px; + text-align: center; + line-height: 100px; + background-color: #eeeeee; + user-select: none; +} diff --git a/tools/playground/samples/components/components.js b/tools/playground/samples/components/components.js new file mode 100644 index 00000000..0a1f3478 --- /dev/null +++ b/tools/playground/samples/components/components.js @@ -0,0 +1,26 @@ +// In this example, we show how components can be defined and created. +import { Component, useState, mount } from "@odoo/owl"; + +class Greeter extends Component { + static template = "Greeter"; + + setup() { + this.state = useState({ word: 'Hello' }); + } + + toggle() { + this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'; + } +} + +// Main root component +class Root extends Component { + static components = { Greeter }; + static template = "Root" + + setup() { + this.state = useState({ name: 'World'}); + } +} + +mount(Root, document.body, { templates: TEMPLATES, dev: true }); diff --git a/tools/playground/samples/components/components.xml b/tools/playground/samples/components/components.xml new file mode 100644 index 00000000..1caaf12a --- /dev/null +++ b/tools/playground/samples/components/components.xml @@ -0,0 +1,9 @@ + +
    + , +
    + + + + +
    diff --git a/tools/playground/samples/custom_hooks/custom_hooks.css b/tools/playground/samples/custom_hooks/custom_hooks.css new file mode 100644 index 00000000..311149f8 --- /dev/null +++ b/tools/playground/samples/custom_hooks/custom_hooks.css @@ -0,0 +1,6 @@ + +button { + width: 120px; + height: 35px; + font-size: 16px; +} diff --git a/tools/playground/samples/custom_hooks/custom_hooks.js b/tools/playground/samples/custom_hooks/custom_hooks.js new file mode 100644 index 00000000..52a72fda --- /dev/null +++ b/tools/playground/samples/custom_hooks/custom_hooks.js @@ -0,0 +1,40 @@ +// In this example, we show how hooks can be used or defined. +import { Component, mount, useState, onWillDestroy } from "@odoo/owl"; + +// We define here a custom behaviour: this hook tracks the state of the mouse +// position +function useMouse() { + const position = useState({x:0, y: 0}); + + function update(e) { + position.x = e.clientX; + position.y = e.clientY; + } + window.addEventListener('mousemove', update); + onWillDestroy(() => { + window.removeEventListener('mousemove', update); + }); + + return position; +} + + +// Main root component +class Root extends Component { + static template = "Root"; + + setup() { + // simple state hook (reactive object) + this.counter = useState({ value: 0 }); + + // this hooks is bound to the 'mouse' property. + this.mouse = useMouse(); + } + + increment() { + this.counter.value++; + } +} + +// Application setup +mount(Root, document.body, { templates: TEMPLATES, dev: true }); diff --git a/tools/playground/samples/custom_hooks/custom_hooks.xml b/tools/playground/samples/custom_hooks/custom_hooks.xml new file mode 100644 index 00000000..1522cfe7 --- /dev/null +++ b/tools/playground/samples/custom_hooks/custom_hooks.xml @@ -0,0 +1,7 @@ + + +
    + +
    Mouse: ,
    +
    +
    diff --git a/tools/playground/samples/form/form.js b/tools/playground/samples/form/form.js new file mode 100644 index 00000000..345c118b --- /dev/null +++ b/tools/playground/samples/form/form.js @@ -0,0 +1,22 @@ +// This example illustrate how the t-model directive can be used to synchronize +// data between html inputs (and select/textareas) and the state of a component. +// Note that there are two controls with t-model="color": they are totally +// synchronized. +import { Component, useState, mount } from "@odoo/owl"; + +class Form extends Component { + static template = "Form"; + + setup() { + this.state = useState({ + text: "", + othertext: "", + number: 11, + color: "", + bool: false + }); + } +} + +// Application setup +mount(Form, document.body, { templates: TEMPLATES, dev: true }); diff --git a/tools/playground/samples/form/form.xml b/tools/playground/samples/form/form.xml new file mode 100644 index 00000000..1693eb9e --- /dev/null +++ b/tools/playground/samples/form/form.xml @@ -0,0 +1,37 @@ + +
    +

    Form

    +
    + Text (immediate): +
    +
    + Other text (lazy): +
    +
    + Number: +
    +
    + Boolean: +
    +
    + Color, with a select: + +
    +
    + Color, with radio buttons: + + +
    +
    +

    State

    +
    Text:
    +
    Other Text:
    +
    Number:
    +
    Boolean: TrueFalse
    +
    Color:
    +
    +
    diff --git a/tools/playground/samples/jsconfig.json b/tools/playground/samples/jsconfig.json new file mode 100644 index 00000000..2d56cbb6 --- /dev/null +++ b/tools/playground/samples/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "paths": { + "owl": ["../../../src/index.ts"], + }, + } +} \ No newline at end of file diff --git a/tools/playground/samples/lifecycle_demo/lifecycle_demo.css b/tools/playground/samples/lifecycle_demo/lifecycle_demo.css new file mode 100644 index 00000000..589db82b --- /dev/null +++ b/tools/playground/samples/lifecycle_demo/lifecycle_demo.css @@ -0,0 +1,11 @@ +button { + font-size: 18px; + margin: 5px; +} + +.demo { + margin: 10px; + padding: 10px; + background-color: #dddddd; + width: 250px; +} diff --git a/tools/playground/samples/lifecycle_demo/lifecycle_demo.js b/tools/playground/samples/lifecycle_demo/lifecycle_demo.js new file mode 100644 index 00000000..ee2eec87 --- /dev/null +++ b/tools/playground/samples/lifecycle_demo/lifecycle_demo.js @@ -0,0 +1,68 @@ +// This example shows all the possible lifecycle hooks +// +// The root component controls a sub component (DemoComponent). It logs all its lifecycle +// methods in the console. Try modifying its state by clicking on it, or by +// clicking on the two main buttons, and look into the console to see what +// happens. +import { + Component, + useState, + mount, + useComponent, + onWillStart, + onMounted, + onWillUnmount, + onWillUpdateProps, + onPatched, + onWillPatch, + onWillRender, + onRendered, + onWillDestroy, +} from "@odoo/owl"; + +function useLogLifecycle() { + const component = useComponent(); + const name = component.constructor.name; + onWillStart(() => console.log(`${name}:willStart`)); + onMounted(() => console.log(`${name}:mounted`)); + onWillUpdateProps(() => console.log(`${name}:willUpdateProps`)); + onWillRender(() => console.log(`${name}:willRender`)); + onRendered(() => console.log(`${name}:rendered`)); + onWillPatch(() => console.log(`${name}:willPatch`)); + onPatched(() => console.log(`${name}:patched`)); + onWillUnmount(() => console.log(`${name}:willUnmount`)); + onWillDestroy(() => console.log(`${name}:willDestroy`)); +} + +class DemoComponent extends Component { + static template = "DemoComponent"; + + setup() { + useLogLifecycle(); + this.state = useState({ n: 0 }); + } + increment() { + this.state.n++; + } +} + +class Root extends Component { + static template = "Root"; + static components = { DemoComponent }; + + setup() { + useLogLifecycle(); + this.state = useState({ n: 0, flag: true }); + } + + increment() { + this.state.n++; + } + + toggleSubComponent() { + this.state.flag = !this.state.flag; + } +} + +mount(Root, document.body, { templates: TEMPLATES, dev: true }); + \ No newline at end of file diff --git a/tools/playground/samples/lifecycle_demo/lifecycle_demo.xml b/tools/playground/samples/lifecycle_demo/lifecycle_demo.xml new file mode 100644 index 00000000..33b0c843 --- /dev/null +++ b/tools/playground/samples/lifecycle_demo/lifecycle_demo.xml @@ -0,0 +1,15 @@ + +
    +
    Demo Sub Component
    +
    (click on me to update me)
    +
    Props: , State: .
    +
    + +
    + + +
    + +
    +
    +
    diff --git a/tools/playground/samples/responsive_app/responsive_app.css b/tools/playground/samples/responsive_app/responsive_app.css new file mode 100644 index 00000000..e43e8e5e --- /dev/null +++ b/tools/playground/samples/responsive_app/responsive_app.css @@ -0,0 +1,56 @@ +body { + margin: 0; +} + +.app { + height: 100%; + flex-direction: column; +} + +.app.desktop { + display: flex; +} + +.navbar { + flex: 0 0 30px; + height: 30px; + background-color: cadetblue; + color: white; + line-height: 30px; +} + +.controlpanel { + flex: 0 0 100px; + height: 100px; + background-color: #dddddd; + padding: 8px; +} + +.content-wrapper { + flex: 1 1 auto; + position: relative; +} + +.content { + display: flex; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.formview { + overflow-y: auto; + flex: 1 1 60%; + min-height: 200px; + padding: 8px; +} + +.chatter { + overflow-y: auto; + flex: 1 1 40%; + background-color: #eeeeee; + color: #333333; + padding: 8px; +} diff --git a/tools/playground/samples/responsive_app/responsive_app.js b/tools/playground/samples/responsive_app/responsive_app.js new file mode 100644 index 00000000..7a2134cc --- /dev/null +++ b/tools/playground/samples/responsive_app/responsive_app.js @@ -0,0 +1,118 @@ +// In this example, we show how one can design an application that is responsive: +// its UI is different in mobile mode or in desktop mode. +// +// The main idea is to have a "isMobile" key in the environment, then listen +// to resize events and update the env if needed. Then, the whole interface +// will be updated, creating and destroying components as needed. +// +// To see this in action, try resizing the window. The application will switch +// to mobile mode whenever it has less than 768px. +import { Component, useState, mount, reactive, useEnv } from "@odoo/owl"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +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); + } + }; +} + +//------------------------------------------------------------------------------ +// Responsive hook +//------------------------------------------------------------------------------ + +function createUI() { + const getIsMobile = () => window.innerWidth <= 768; + + const ui = reactive({ isMobile: getIsMobile() }); + + const updateEnv = debounce(() => { + const isMobile = getIsMobile(); + if (ui.isMobile !== isMobile) { + ui.isMobile = isMobile; + } + }, 15); + window.addEventListener("resize", updateEnv); + return ui; +} + +function useUI() { + const env = useEnv(); + return useState(env.ui); +} + + +//------------------------------------------------------------------------------ +// Components +//------------------------------------------------------------------------------ +class Navbar extends Component { + static template = "Navbar"; +} + +class MobileSearchView extends Component { + static template = "MobileSearchView"; +} + +class ControlPanel extends Component { + static template = "ControlPanel"; + static components = { MobileSearchView }; + setup() { + this.ui = useUI(); + } +} + +class AdvancedComponent extends Component { + static template = "AdvancedComponent"; +} + +class FormView extends Component { + static template = "FormView"; + static components = { AdvancedComponent }; + setup() { + this.ui = useUI(); + } +} + +class Chatter extends Component { + static template = "Chatter"; + + setup() { + this.messages = Array.from(Array(100).keys()); + } +} + +class Root extends Component { + static template = "Root"; + static components = { Navbar, ControlPanel, FormView, Chatter }; + + setup() { + this.ui = useUI(); + } +} + + + +//------------------------------------------------------------------------------ +// Application Startup +//------------------------------------------------------------------------------ +const env = { + ui: createUI() +}; + +mount(Root, document.body, { templates: TEMPLATES, env, dev: true }); diff --git a/tools/playground/samples/responsive_app/responsive_app.xml b/tools/playground/samples/responsive_app/responsive_app.xml new file mode 100644 index 00000000..ccb2da9b --- /dev/null +++ b/tools/playground/samples/responsive_app/responsive_app.xml @@ -0,0 +1,42 @@ + + + +
    +

    Control Panel

    + +
    + +
    +

    Form View

    + +
    + +
    +

    Chatter

    +
    Message
    +
    + +
    Mobile searchview
    + +
    + This component is only created in desktop mode. + +
    + + + + + +
    + + +
    +
    + +
    +
    + + + +
    +
    diff --git a/tools/playground/samples/single_file_component/single_file_component.js b/tools/playground/samples/single_file_component/single_file_component.js new file mode 100644 index 00000000..20ba9e5e --- /dev/null +++ b/tools/playground/samples/single_file_component/single_file_component.js @@ -0,0 +1,28 @@ +// This example illustrates how one can write Owl components with +// inline templates. + +import { Component, useState, xml, mount } from "@odoo/owl"; + +// Counter component +class Counter extends Component { + static template = xml` + `; + + state = useState({ value: 0 }) +} + +// Root +class Root extends Component { + static template = xml` +
    + + +
    `; + + static components = { Counter }; +} + +// Application setup +mount(Root, document.body, { templates: TEMPLATES, dev: true}); diff --git a/tools/playground/samples/slots/slots.css b/tools/playground/samples/slots/slots.css new file mode 100644 index 00000000..786e938c --- /dev/null +++ b/tools/playground/samples/slots/slots.css @@ -0,0 +1,43 @@ +.main { + display: flex; +} + +.card { + display: flex; + flex-direction: column; + background-color: #eeeeee; + width: 200px; + height: 100px; + margin: 10px; + border: 1px solid gray; +} + +.card.full { + height: 100px; +} + +.card.small { + height: 25px; +} + +.card-title { + flex: 0 0 25px; + font-weight: bold; + background-color: darkcyan; + color: white; + padding: 2px; +} + +.card-title button { + float: right; +} + +.card-content { + flex: 1 1 auto; + padding: 5px; + border-top: 1px solid white; +} + +.card-footer { + border-top: 1px solid white; +} diff --git a/tools/playground/samples/slots/slots.js b/tools/playground/samples/slots/slots.js new file mode 100644 index 00000000..58eac769 --- /dev/null +++ b/tools/playground/samples/slots/slots.js @@ -0,0 +1,49 @@ +// We show here how slots can be used to create generic components. +// In this example, the Card component is basically only a container. It is not +// aware of its content. It just knows where it should be (with t-slot). +// The parent component define the content with t-set-slot. +// +// Note that the t-on-click event, defined in the Root template, is executed in +// the context of the Root component, even though it is inside the Card component +import { Component, useState, mount } from "@odoo/owl"; + +class Card extends Component { + static template = "Card"; + + setup() { + this.state = useState({ showContent: true }); + } + + toggleDisplay() { + this.state.showContent = !this.state.showContent; + } +} + +class Counter extends Component { + static template = "Counter"; + + setup() { + this.state = useState({val: 1}); + } + + inc() { + this.state.val++; + } +} + +// Main root component +class Root extends Component { + static template = "Root" + static components = { Card, Counter }; + + setup() { + this.state = useState({a: 1, b: 3}); + } + + inc(key, delta) { + this.state[key] += delta; + } +} + +// Application setup +mount(Root, document.body, { templates: TEMPLATES, dev: true}); diff --git a/tools/playground/samples/slots/slots.xml b/tools/playground/samples/slots/slots.xml new file mode 100644 index 00000000..34227137 --- /dev/null +++ b/tools/playground/samples/slots/slots.xml @@ -0,0 +1,33 @@ + +
    +
    + +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    + + Content of card 1... [] + + + + +
    Card 2... []
    + +
    + +
    +
    +
    diff --git a/tools/playground/samples/todo_app/todo_app.css b/tools/playground/samples/todo_app/todo_app.css new file mode 100644 index 00000000..bdfd38a8 --- /dev/null +++ b/tools/playground/samples/todo_app/todo_app.css @@ -0,0 +1,379 @@ +html,body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +:focus { + outline: 0; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.toggle-all { + width: 1px; + height: 1px; + border: none; /* Mobile Safari */ + opacity: 0; + position: absolute; + right: 100%; + bottom: 100%; +} + +.toggle-all + label { + width: 60px; + height: 34px; + font-size: 0; + position: absolute; + top: -52px; + left: -13px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.toggle-all + label:before { + content: "❯"; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +.toggle-all:checked + label:before { + color: #737373; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: calc(100% - 43px); + padding: 12px 16px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle { + opacity: 0; +} + +.todo-list li .toggle + label { + /* + Firefox requires \`#\` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 + IE and Edge requires *everything* to be escaped to render, so we do that instead of just the \`#\` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ + */ + background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center left; +} + +.todo-list li .toggle:checked + label { + background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E"); +} + +.todo-list li label { + word-break: break-all; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover { + color: #af5b5e; +} + +.todo-list li .destroy:after { + content: "×"; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ""; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #bfbfbf; + font-size: 10px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio: 0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} + +.filters a { + cursor: pointer; +} diff --git a/tools/playground/samples/todo_app/todo_app.js b/tools/playground/samples/todo_app/todo_app.js new file mode 100644 index 00000000..bc602d09 --- /dev/null +++ b/tools/playground/samples/todo_app/todo_app.js @@ -0,0 +1,182 @@ +// This example is an implementation of the TodoList application, from the +// www.todomvc.com project. This is a non trivial application with some +// interesting user interactions. It uses the local storage for persistence. +// +// In this implementation, we use the owl reactivity mechanism. +import { Component, useState, mount, useRef, reactive, useEnv, useEffect } from "@odoo/owl"; + +//------------------------------------------------------------------------------ +// Constants, helpers +//------------------------------------------------------------------------------ +const ENTER_KEY = 13; +const ESC_KEY = 27; + +function useAutofocus(name) { + let ref = useRef(name); + useEffect(el => el && el.focus(), () => [ref.el]); +} + +function useStore() { + const env = useEnv(); + return useState(env.store); +} + +//------------------------------------------------------------------------------ +// Task store +//------------------------------------------------------------------------------ +class TaskList { + constructor(tasks) { + this.tasks = tasks || []; + const taskIds = this.tasks.map((t) => t.id); + this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1; + } + + addTask(text) { + text = text.trim(); + if (text) { + const task = { + id: this.nextId++, + text: text, + isCompleted: false, + }; + this.tasks.push(task); + } + } + + toggleTask(task) { + task.isCompleted = !task.isCompleted; + } + + toggleTask(id) { + const task = this.tasks.find(t => t.id === id); + task.isCompleted = !task.isCompleted; + } + + toggleAll(value) { + for (let task of this.tasks) { + task.isCompleted = value; + } + } + + clearCompleted() { + const tasks = this.tasks.filter(t => t.isCompleted); + for (let task of tasks) { + this.deleteTask(task); + } + } + + deleteTask(id) { + const index = this.tasks.findIndex((t) => t.id === id); + this.tasks.splice(index, 1); + } + + updateTask(id, text) { + const value = text.trim(); + if (!value) { + this.deleteTask(id); + } else { + const task = this.tasks.find(t => t.id === id); + task.text = value; + } + } +} + +function createTaskStore() { + const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks)); + const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]"); + const taskStore = reactive(new TaskList(initialTasks), saveTasks); + saveTasks(); + return taskStore; +} + +//------------------------------------------------------------------------------ +// Todo +//------------------------------------------------------------------------------ +class Todo extends Component { + static template = "Todo"; + + setup() { + useAutofocus("input"); + this.store = useStore(); + this.state = useState({ + isEditing: false + }); + } + + handleKeyup(ev) { + if (ev.keyCode === ENTER_KEY) { + this.updateText(ev.target.value); + } + if (ev.keyCode === ESC_KEY) { + ev.target.value = this.props.text; + this.state.isEditing = false; + } + } + + handleBlur(ev) { + this.updateText(ev.target.value); + } + + updateText(text) { + this.store.updateTask(this.props.id, text); + this.state.isEditing = false; + } +} + +//------------------------------------------------------------------------------ +// TodoList +//------------------------------------------------------------------------------ +class TodoList extends Component { + static template = "TodoList"; + static components = { Todo }; + + setup() { + this.store = useStore(); + this.state = useState({ filter: "all" }); + } + + get displayedTasks() { + const tasks = this.store.tasks; + switch (this.state.filter) { + case "active": + return tasks.filter((t) => !t.isCompleted); + case "completed": + return tasks.filter((t) => t.isCompleted); + case "all": + return tasks; + } + } + + get allChecked() { + return this.store.tasks.every(todo => todo.isCompleted); + } + + get remaining() { + return this.store.tasks.filter(todo => !todo.isCompleted).length; + } + + get remainingText() { + const items = this.remaining < 2 ? "item" : "items"; + return ` ${items} left`; + } + + addTodo(ev) { + if (ev.keyCode === ENTER_KEY) { + const text = ev.target.value; + if (text.trim()) { + this.store.addTask(text); + } + ev.target.value = ""; + } + } + + setFilter(filter) { + this.state.filter = filter; + } +} + +//------------------------------------------------------------------------------ +// App Initialization +//------------------------------------------------------------------------------ +const env = { store: createTaskStore() }; +mount(TodoList, document.body, { env, templates: TEMPLATES, dev: true }); diff --git a/tools/playground/samples/todo_app/todo_app.xml b/tools/playground/samples/todo_app/todo_app.xml new file mode 100644 index 00000000..1ed4d9f7 --- /dev/null +++ b/tools/playground/samples/todo_app/todo_app.xml @@ -0,0 +1,50 @@ + +
    +
    +

    todos

    + +
    +
    + + +
      + + + +
    +
    + +
    + +
  • +
    + + + +
    + +
  • +
    diff --git a/tools/playground/samples/window_manager/window_manager.css b/tools/playground/samples/window_manager/window_manager.css new file mode 100644 index 00000000..92ee4226 --- /dev/null +++ b/tools/playground/samples/window_manager/window_manager.css @@ -0,0 +1,70 @@ +body { + margin: 0; +} + +.app { + width: 100%; + height: 100%; + display: grid; + grid-template-rows: auto 50px; +} + +.window-manager { + position: relative; + width: 100%; + height: 100%; + background-color: #eeeeee; + overflow: hidden; +} + +.menubar { + background-color: #875a7b; + color: white; +} + +.menubar button { + height: 40px; + font-size: 18px; + margin: 5px; +} + +.window { + display: grid; + grid-template-rows: 30px auto; + border: 1px solid gray; + background-color: white; + position: absolute; + box-shadow: 1px 1px 2px 1px grey; +} + +.window.dragging { + opacity: 0.75; +} + +.window .header { + background-color: #875a7b; + display: grid; + grid-template-columns: auto 24px; + color: white; + line-height: 30px; + padding-left: 5px; + cursor: default; + user-select: none; +} + +.window .header .close { + cursor: pointer; + font-size: 22px; + padding-left: 4px; + padding-right: 4px; + font-weight: bold; +} + +.counter { + font-size: 20px; +} +.counter button { + width: 80px; + height:40px; + font-size: 20px; +} diff --git a/tools/playground/samples/window_manager/window_manager.js b/tools/playground/samples/window_manager/window_manager.js new file mode 100644 index 00000000..9783beea --- /dev/null +++ b/tools/playground/samples/window_manager/window_manager.js @@ -0,0 +1,192 @@ +// This example is slightly more complex than usual. We demonstrate +// here a way to manage sub windows in Owl, declaratively. This is still just a +// demonstration. Managing windows can be as complex as we want. For example, +// we could implement the following features: +// - resizing windows +// - minimizing windows +// - configuration options for windows to make a window non resizeable +// - minimal width/height +// - better heuristic for initial window position +// - ... +import { Component, useState, mount, useRef, reactive, useEnv, onMounted } from "@odoo/owl"; + +// ----------------------------------------------------------------------------- +// Window manager code +// ----------------------------------------------------------------------------- + +class WindowManager { + // contains all components with metadata + static Windows = {}; + windows = {}; // mapping id => info + nextId = 1; + + add(type) { + const Comp = WindowManager.Windows[type]; + const left = 50 + Math.round(Math.random()*(window.innerWidth - 50 - Comp.defaultWidth)); + const top = 50 + Math.round(Math.random()*(window.innerHeight - 100 - Comp.defaultHeight)); + const id = this.nextId++; + this.windows[id] = { + id, + title: Comp.defaultTitle, + width: Comp.defaultWidth, + height: Comp.defaultHeight, + left, + top, + Component: Comp, + }; + } + + close(id) { + delete this.windows[id]; + } + + updatePosition(id, left, top) { + const w = this.windows[id]; + w.left = left; + w.top = top; + } + + getWindows() { + return Object.values(this.windows); + } +} + +function createWindowService() { + return reactive(new WindowManager()); +} + +function useWindowService() { + const env = useEnv(); + return useState(env.windowService); +} + +// ----------------------------------------------------------------------------- +// Generic Window Component +// ----------------------------------------------------------------------------- + +class Window extends Component { + static template = "Window"; + static nextZIndex = 1; + zIndex = 0; + + setup() { + this.windowService = useWindowService(); + this.root = useRef('root'); + onMounted(this.updateZIndex); + } + + get style() { + let { width, height, top, left } = this.props.info; + return `width: ${width}px;height: ${height}px;top:${top}px;left:${left}px;z-index:${this.zIndex}`; + } + + close() { + this.windowService.close(this.props.info.id); + } + + startDragAndDrop(ev) { + this.updateZIndex(); + const self = this; + const root = this.root; + + const el = root.el; + el.classList.add('dragging'); + + const current = this.props.info; + const offsetX = current.left - ev.pageX; + const offsetY = current.top - ev.pageY; + let left, top; + + window.addEventListener("mousemove", moveWindow); + window.addEventListener("mouseup", stopDnD, { once: true }); + + function moveWindow(ev) { + left = Math.max(offsetX + ev.pageX, 0); + top = Math.max(offsetY + ev.pageY, 0); + el.style.left = `${left}px`; + el.style.top = `${top}px`; + } + function stopDnD() { + window.removeEventListener("mousemove", moveWindow); + el.classList.remove('dragging'); + + if (top !== undefined && left !== undefined) { + self.windowService.updatePosition(current.id, left, top); + } + } + } + + updateZIndex() { + this.zIndex = Window.nextZIndex++; + this.root.el.style['z-index'] = this.zIndex; + } +} + +// ----------------------------------------------------------------------------- +// Two concrete Window type implementations +// ----------------------------------------------------------------------------- + +class HelloWorld extends Component { + static template = "HelloWorld"; + static defaultTitle = "Hello Owl!"; + static defaultWidth = 200; + static defaultHeight = 100; +} + + +class Counter extends Component { + static template = "Counter"; + static defaultTitle = "Click Counter"; + static defaultWidth = 300; + static defaultHeight = 120; + + state = useState({ value: 0 }); + + inc() { + this.state.value++; + } +} + +// register window components +WindowManager.Windows.Hello = HelloWorld; +WindowManager.Windows.Counter = Counter; + +// ----------------------------------------------------------------------------- +// Window Container +// ----------------------------------------------------------------------------- + +class WindowContainer extends Component { + static template = "WindowContainer"; + static components = { Window }; + + setup() { + this.windowService = useWindowService(); + } +} + +// ----------------------------------------------------------------------------- +// Root Component +// ----------------------------------------------------------------------------- + +class Root extends Component { + static template = "Root"; + static components = { WindowContainer }; + + setup() { + this.windowService = useWindowService(); + } + + addWindow(type) { + this.windowService.add(type); + } +} + +// ----------------------------------------------------------------------------- +// Setup +// ----------------------------------------------------------------------------- + +const env = { + windowService: createWindowService(), +}; + +mount(Root, document.body, { templates: TEMPLATES, env, dev: true }); diff --git a/tools/playground/samples/window_manager/window_manager.xml b/tools/playground/samples/window_manager/window_manager.xml new file mode 100644 index 00000000..e5a1e242 --- /dev/null +++ b/tools/playground/samples/window_manager/window_manager.xml @@ -0,0 +1,32 @@ + +
    +
    + + × +
    + +
    + +
    + + + +
    + +
    + + +
    + +
    + Some content here... +
    + +
    + + +
    +
    diff --git a/tools/playground/standalone_app/app.py b/tools/playground/standalone_app/app.py new file mode 100644 index 00000000..42505fcd --- /dev/null +++ b/tools/playground/standalone_app/app.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +import threading +import time + +from http.server import SimpleHTTPRequestHandler, HTTPServer + +httpd = None +def start_server(): + global httpd + 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) diff --git a/tools/playground/standalone_app/index.html b/tools/playground/standalone_app/index.html new file mode 100644 index 00000000..88e25ec9 --- /dev/null +++ b/tools/playground/standalone_app/index.html @@ -0,0 +1,22 @@ + + + + + OWL App + + + + + + + +
    +This app requires a web server, simply opening the html file using your browser will not work.
    +You can run a simple web server with one of the following:
    +- if you have python3 installed, run app.py with python3 or run 'python3 -m http.server' in the app folder
    +- if you have node/npm installed, run 'npx serve' in the app folder
    +- if you have php installed, run 'php -S localhost:8080 -t .' in the app folder
    +    
    + + + \ No newline at end of file diff --git a/tools/playground/templates.xml b/tools/playground/templates.xml index 602d5e81..2054da23 100644 --- a/tools/playground/templates.xml +++ b/tools/playground/templates.xml @@ -1,5 +1,5 @@ -
    +
    -
    +
    @@ -37,19 +37,19 @@ css="state.css" xml="state.xml" resizeable="true" - onUpdatePanelHeight="updatePanelHeight" + updatePanelHeight.bind="updatePanelHeight" updateCode="updateCode"/>
    -
    +
    🦉 Odoo Web Library 🦉
    v
    -

    Note: these examples are using recent features of Javascript, and require a recent browser to work without a transpilation step! - For example, it makes use of class fields and class static fields. These examples should work in a recent Chrome version. +

    + Note: these examples make use of recent features of Javascript, and require a recent browser to work without a transpilation step! Among other things, it uses class fields and import maps. If you encounter issues, make sure your browser is up to date.

    diff --git a/tools/playground/utils.js b/tools/playground/utils.js index 96f8dd4e..88b20cb8 100644 --- a/tools/playground/utils.js +++ b/tools/playground/utils.js @@ -18,12 +18,6 @@ export function debounce(func, wait, immediate) { }; } -export function useBus(bus, evName, cb) { - bus.addEventListener(evName, cb); - owl.onWillUnmount(() => { - bus.removeEventListener(evName, cb); - }); -} const loadedScripts = {}; diff --git a/tools/server.py b/tools/server.py index 8aba2c2a..5b9a51de 100644 --- a/tools/server.py +++ b/tools/server.py @@ -15,8 +15,8 @@ URL = 'http://{0}:{1}/tools/playground'.format(HOST, PORT) # we want to update the playground. class OWLHandler(SimpleHTTPRequestHandler): def do_GET(self): - if self.path == '/tools/owl.js': - self.path = '/dist/owl.iife.js' + if self.path == '/tools/owl.js' or self.path == '/tools/playground/owl.js': + self.path = '/dist/owl.es.js' return SimpleHTTPRequestHandler.do_GET(self) def end_headers(self):