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):