diff --git a/examples/todoapp/app.css b/examples/todoapp/app.css new file mode 100644 index 00000000..4dcc858c --- /dev/null +++ b/examples/todoapp/app.css @@ -0,0 +1,376 @@ +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; + } +} diff --git a/examples/todoapp/components/TodoApp.js b/examples/todoapp/components/TodoApp.js new file mode 100644 index 00000000..20d1fd57 --- /dev/null +++ b/examples/todoapp/components/TodoApp.js @@ -0,0 +1,43 @@ +import { TodoItem } from "./TodoItem.js"; + +const { StoreMixin, Component } = odoo.core; + +export class TodoApp extends StoreMixin(Component) { + template = "todoapp"; + widgets = { TodoItem }; + state = { filter: "all" }; + + get todos() { + return this.env.store.state.todos; + } + + get allChecked() { + return this.todos.every(todo => todo.done); + } + + get remaining() { + return this.todos.filter(todo => !todo.done).length; + } + + get remainingText() { + return (this.remaining < 2 ? "item" : "items") + " left"; + } + + addTodo(ev) { + if (ev.keyCode === 13) { + const text = ev.target.value; + if (text.trim()) { + this.env.store.dispatch("addTodo", text); + } + ev.target.value = ""; + } + } + + clearCompleted() { + this.env.store.dispatch("clearCompleted"); + } + + toggleAll() { + this.env.store.dispatch("toggleAll", !this.allChecked); + } +} diff --git a/examples/todoapp/components/TodoItem.js b/examples/todoapp/components/TodoItem.js new file mode 100644 index 00000000..acb92400 --- /dev/null +++ b/examples/todoapp/components/TodoItem.js @@ -0,0 +1,11 @@ +export class TodoItem extends odoo.core.Component { + template = "todoitem"; + + removeTodo() { + this.env.store.dispatch("removeTodo", this.props.id); + } + + toggleTodo() { + this.env.store.dispatch("toggleTodo", this.props.id); + } +} diff --git a/examples/todoapp/index.html b/examples/todoapp/index.html new file mode 100644 index 00000000..7f851575 --- /dev/null +++ b/examples/todoapp/index.html @@ -0,0 +1,16 @@ + + + + + Odoo Demo App + + + + + + + + + + + \ No newline at end of file diff --git a/examples/todoapp/main.js b/examples/todoapp/main.js new file mode 100644 index 00000000..0e77fe0d --- /dev/null +++ b/examples/todoapp/main.js @@ -0,0 +1,26 @@ +import { TodoApp } from "./components/TodoApp.js"; +import { makeStore } from "./store.js"; + +async function makeEnv() { + const result = await fetch("templates.xml"); + if (!result.ok) { + throw new Error("Error while fetching xml templates"); + } + let templates = await result.text(); + templates = templates.replace(//g, ""); + const qweb = new odoo.core.QWeb(); + qweb.loadTemplates(templates); + return { + qweb, + store: makeStore() + }; +} + +document.addEventListener("DOMContentLoaded", async function() { + const env = await makeEnv(); + const app = new TodoApp(env); + + // for debugging purpose + window.app = app; + await app.mount(document.body); +}); diff --git a/examples/todoapp/store.js b/examples/todoapp/store.js new file mode 100644 index 00000000..634b7f78 --- /dev/null +++ b/examples/todoapp/store.js @@ -0,0 +1,66 @@ +//------------------------------------------------------------------------------ +// ACTIONS +//------------------------------------------------------------------------------ + +const actions = { + addTodo({ commit }, text) { + commit("addTodo", text); + }, + removeTodo({ commit }, id) { + commit("removeTodo", id); + }, + toggleTodo({ state, commit }, id) { + const todo = state.todos.find(t => t.id === id); + commit("editTodo", { id, done: !todo.done }); + }, + clearCompleted({ state, commit }) { + state.todos + .filter(todo => todo.done) + .forEach(todo => { + commit("removeTodo", todo.id); + }); + }, + toggleAll({ state, commit }, done) { + state.todos.forEach(todo => { + commit("editTodo", { id: todo.id, done }); + }); + } +}; + +//------------------------------------------------------------------------------ +// MUTATIONS +//------------------------------------------------------------------------------ + +const mutations = { + addTodo(state, text) { + const id = state.nextId++; + const todo = { id, text, done: false }; + state.todos.push(todo); + }, + removeTodo(state, id) { + const index = state.todos.findIndex(t => t.id === id); + state.todos.splice(index, 1); + }, + editTodo(state, { id, text, done }) { + const todo = state.todos.find(t => t.id === id); + if (text !== undefined) { + todo.text = text; + } + if (done !== undefined) { + todo.done = done; + } + } +}; + +class TodoStore extends odoo.core.Store { + commit(...args) { + super.commit(...args); + window.localStorage.setItem("todos", JSON.stringify(this.state.todos)); + } +} +export function makeStore() { + const todos = JSON.parse(window.localStorage.getItem("todos") || "[]"); + const nextId = Math.max(0, ...todos.map(t => t.id || 0)) + 1; + const state = { todos, nextId }; + return new TodoStore({ state, actions, mutations }); +} diff --git a/examples/todoapp/templates.xml b/examples/todoapp/templates.xml new file mode 100644 index 00000000..9fcd58b4 --- /dev/null +++ b/examples/todoapp/templates.xml @@ -0,0 +1,44 @@ + + + +
+ +
+

todos

+ +
+ +
+ + +
    + + + +
+
+ +
+ + + + + + + +
+
+ +
  • +
    + + + +
    +
  • + +
    diff --git a/libs/snabbdom/src/modules/attributes.ts b/libs/snabbdom/src/modules/attributes.ts index 47612e8c..b39e0ad5 100755 --- a/libs/snabbdom/src/modules/attributes.ts +++ b/libs/snabbdom/src/modules/attributes.ts @@ -1,25 +1,30 @@ -import {VNode, VNodeData} from '../vnode'; -import {Module} from './module'; +import { VNode, VNodeData } from "../vnode"; +import { Module } from "./module"; // because those in TypeScript are too restrictive: https://github.com/Microsoft/TSJS-lib-generator/pull/237 declare global { interface Element { setAttribute(name: string, value: string | number | boolean): void; - setAttributeNS(namespaceURI: string, qualifiedName: string, value: string | number | boolean): void; + setAttributeNS( + namespaceURI: string, + qualifiedName: string, + value: string | number | boolean + ): void; } } -export type Attrs = Record +export type Attrs = Record; -const xlinkNS = 'http://www.w3.org/1999/xlink'; -const xmlNS = 'http://www.w3.org/XML/1998/namespace'; +const xlinkNS = "http://www.w3.org/1999/xlink"; +const xmlNS = "http://www.w3.org/XML/1998/namespace"; const colonChar = 58; const xChar = 120; function updateAttrs(oldVnode: VNode, vnode: VNode): void { - var key: string, elm: Element = vnode.elm as Element, - oldAttrs = (oldVnode.data as VNodeData).attrs, - attrs = (vnode.data as VNodeData).attrs; + var key: string, + elm: Element = vnode.elm as Element, + oldAttrs = (oldVnode.data as VNodeData).attrs, + attrs = (vnode.data as VNodeData).attrs; if (!oldAttrs && !attrs) return; if (oldAttrs === attrs) return; @@ -30,6 +35,7 @@ function updateAttrs(oldVnode: VNode, vnode: VNode): void { for (key in attrs) { const cur = attrs[key]; const old = oldAttrs[key]; + debugger; if (old !== cur) { if (cur === true) { elm.setAttribute(key, ""); @@ -60,5 +66,8 @@ function updateAttrs(oldVnode: VNode, vnode: VNode): void { } } -export const attributesModule = {create: updateAttrs, update: updateAttrs} as Module; +export const attributesModule = { + create: updateAttrs, + update: updateAttrs +} as Module; export default attributesModule; diff --git a/package.json b/package.json index 95148fe8..6f66d355 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,12 @@ "example:benchmarks:build:js": "cpx \"examples/benchmarks/{*.html,*.js,*.css}\" dist/examples/benchmarks", "example:benchmarks:build": "npm-run-all --parallel example:benchmarks:build:*", "example:benchmarks:serve": "live-server --entry-file=index.html dist/examples/benchmarks/", - "example:benchmarks:dev": "npm-run-all --parallel \"example:benchmarks:build:* -- --watch\" example:benchmarks:serve" + "example:benchmarks:dev": "npm-run-all --parallel \"example:benchmarks:build:* -- --watch\" example:benchmarks:serve", + "example:todoapp:build:core": "npm run build && cpx \"dist/core.js\" dist/examples/todoapp", + "example:todoapp:build:js": "cpx \"examples/todoapp/{*.html,**/*.js,*.css,*.xml}\" dist/examples/todoapp", + "example:todoapp:build": "npm-run-all --parallel example:todoapp:build:*", + "example:todoapp:serve": "live-server --entry-file=index.html dist/examples/todoapp/", + "example:todoapp:dev": "npm-run-all --parallel \"example:todoapp:build:* -- --watch\" example:todoapp:serve" }, "repository": { "type": "git",