diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 43aefc921..000000000 --- a/.eslintrc +++ /dev/null @@ -1,47 +0,0 @@ -{ - "env": { - "browser": true, - "node": true, - "es2022": true - }, - "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint"], - "parserOptions": { - "sourceType": "module" - }, - "root": true, - "rules": { - "no-restricted-globals": ["error", "event", "self"], - "no-const-assign": ["error"], - "no-debugger": ["error"], - "no-dupe-class-members": ["error"], - "no-dupe-keys": ["error"], - "no-dupe-args": ["error"], - "no-dupe-else-if": ["error"], - "no-unsafe-negation": ["error"], - "no-duplicate-imports": ["error"], - "valid-typeof": ["error"], - "@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": false, "caughtErrors": "all" }], - "no-restricted-syntax": [ - "error", - { - "selector": "MemberExpression[object.name='test'][property.name='only']", - "message": "test.only(...) is forbidden", - }, - { - "selector": "MemberExpression[object.name='describe'][property.name='only']", - "message": "describe.only(...) is forbidden", - } - ], - }, - "globals": { - "describe": true, - "expect": true, - "test": true, - "beforeEach": true, - "beforeAll": true, - "afterEach": true, - "afterAll": true, - "jest": true, - }, -} diff --git a/README.md b/README.md index 4bf8b527d..b34d47f03 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![npm version](https://badge.fury.io/js/@odoo%2Fowl.svg)](https://badge.fury.io/js/@odoo%2Fowl) [![Downloads](https://img.shields.io/npm/dm/@odoo%2Fowl.svg)](https://www.npmjs.com/package/@odoo/owl) -_Class based components with hooks, reactive state and concurrent mode_ +_Class based components with hooks, signals and concurrent mode_ **Try it online!** you can experiment with the Owl framework in an online [playground](https://odoo.github.io/owl/playground). @@ -16,9 +16,10 @@ framework, written in Typescript, taking the best ideas from React and Vue in a simple and consistent way. Owl's main features are: - a declarative component system, -- a fine grained reactivity system similar to Vue, -- hooks -- fragments +- a signal-based reactivity system (signals, computed values, effects), +- a plugin system for sharing state and services, +- hooks, +- fragments, - asynchronous rendering Owl components are defined with ES6 classes and xml templates, uses an @@ -29,6 +30,7 @@ Quick links: - [documentation](#documentation), - [changelog](CHANGELOG.md) (from Owl 1.x to 2.x), +- [Owl 3.x release notes](release_notes.md) (draft), - [playground](https://odoo.github.io/owl/playground) ## Example @@ -36,21 +38,25 @@ Quick links: Here is a short example to illustrate interactive components: ```javascript -const { Component, useState, mount, xml } = owl; +const { Component, signal, mount, xml } = owl; class Counter extends Component { static template = xml` - `; - state = useState({ value: 0 }); + count = signal(0); + + increment() { + this.count.set(this.count() + 1); + } } class Root extends Component { static template = xml` Hello Owl - `; + `; static components = { Counter }; } @@ -58,8 +64,8 @@ class Root extends Component { mount(Root, document.body); ``` -Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate). -Also, all examples here uses the [`xml` helper](doc/reference/templates.md#inline-templates) to define inline templates. +Note that the counter component is made reactive with a [`signal`](release_notes.md#signals). +Also, all examples here use the `xml` helper to define inline templates. But this is not mandatory, many applications will load templates separately. More interesting examples can be found on the @@ -67,6 +73,9 @@ More interesting examples can be found on the ## Documentation +Note: the reference documentation below was written for Owl 2.x. The +[Owl 3.x release notes](release_notes.md) describe all changes in detail. + ### Learning Owl Are you new to Owl? This is the place to start! @@ -84,7 +93,6 @@ Are you new to Owl? This is the place to start! - [Concurrency Model](doc/reference/concurrency_model.md) - [Dev mode](doc/reference/app.md#dev-mode) - [Dynamic sub components](doc/reference/component.md#dynamic-sub-components) -- [Environment](doc/reference/environment.md) - [Error Handling](doc/reference/error_handling.md) - [Event Handling](doc/reference/event_handling.md) - [Form Input Bindings](doc/reference/input_bindings.md) @@ -92,7 +100,6 @@ Are you new to Owl? This is the place to start! - [Hooks](doc/reference/hooks.md) - [Loading Templates](doc/reference/app.md#loading-templates) - [Mounting a component](doc/reference/app.md#mount-helper) -- [Portal](doc/reference/portal.md) - [Precompiling templates](doc/reference/precompiling_templates.md) - [Props](doc/reference/props.md) - [Props Validation](doc/reference/props.md#props-validation) @@ -112,6 +119,7 @@ Are you new to Owl? This is the place to start! - [Comparison with React/Vue](doc/miscellaneous/comparison.md) - [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md) - [Changelog (from owl 1.x to 2.x)](CHANGELOG.md) +- [Owl 3.x Release Notes (draft)](release_notes.md) - [Notes on compiled templates](doc/miscellaneous/compiled_template.md) - [Owl devtools extension](doc/tools/devtools.md) @@ -134,16 +142,14 @@ Unzip the owl-devtools.zip file and follow the instructions depending on your br ### Chrome Go to your chrome extensions admin panel, activate developer mode and click on `Load unpacked`. -Select the devtools-chrome folder and that's it, your extension is active! -There is a convenient refresh button on the extension card (still on the same admin page) to update your code. -Do note that if you got some problems, you may need to completly remove and reload the extension to completly refresh the extension. +Select the devtools-chrome folder and that's it, your extension is active! +There is a convenient refresh button on the extension card (still on the same admin page) to update your code. +Do note that if you have problems, you may need to completely remove and reload the extension to fully refresh it. -### Firefox +### Firefox Go to the address about:debugging#/runtime/this-firefox and click on `Load temporary Add-on...`. -Select any file in the devtools-firefox folder and that's it, your extension is active! +Select any file in the devtools-firefox folder and that's it, your extension is active! Here, you can use the reload button to refresh the extension. Note that you may have to open another window or reload your tab to see the extension working. Also note that the extension will only be active on pages that have a sufficient version of owl. - - diff --git a/doc/learning/tutorial_todoapp.md b/doc/learning/tutorial_todoapp.md index 7f606a0ad..0ae298328 100644 --- a/doc/learning/tutorial_todoapp.md +++ b/doc/learning/tutorial_todoapp.md @@ -148,7 +148,7 @@ data and a template to the `App` component: ```js class Root extends Component { - static template = xml/* xml */ ` + static template = xml /* xml */ `
@@ -565,7 +565,7 @@ function createTaskStore() { // Task Component // ------------------------------------------------------------------------- class Task extends Component { - static template = xml/* xml */ ` + static template = xml /* xml */ `
@@ -583,7 +583,7 @@ class Task extends Component { // Root Component // ------------------------------------------------------------------------- class Root extends Component { - static template = xml/* xml */ ` + static template = xml /* xml */ `
@@ -838,7 +838,7 @@ For reference, here is the final code: // Task Component // ------------------------------------------------------------------------- class Task extends Component { - static template = xml/* xml */ ` + static template = xml /* xml */ `
diff --git a/doc/readme.md b/doc/readme.md index 9363ba708..0dcd45d7e 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -34,7 +34,7 @@ Other hooks: - [`useComponent`](reference/hooks.md#usecomponent): return a reference to the current component (useful to create derived hooks) - [`useEffect`](reference/hooks.md#useeffect): define an effect with its dependencies - [`useEnv`](reference/hooks.md#useenv): return a reference to the current env -- [`useExternalListener`](reference/hooks.md#useexternallistener): add a listener outside of a component DOM +- [`useListener`](reference/hooks.md#uselistener): add a listener outside of a component DOM - [`useRef`](reference/hooks.md#useref): get an object representing a reference (`t-ref`) - [`useChildSubEnv`](reference/hooks.md#usesubenv-and-usechildsubenv): extend the current env with additional information (for child components) - [`useSubEnv`](reference/hooks.md#usesubenv-and-usechildsubenv): extend the current env with additional information (for current component and child components) @@ -42,7 +42,6 @@ Other hooks: Utility/helpers: - [`EventBus`](reference/utils.md#eventbus): a simple event bus -- [`loadFile`](reference/utils.md#loadfile): an helper to load a file from the server - [`markup`](reference/templates.md#outputting-data): utility function to define strings that represent html (should not be escaped) - [`status`](reference/component.md#status-helper): utility function to get the status of a component (new, mounted or destroyed) - [`validate`](reference/utils.md#validate): validates if an object satisfies a specified schema diff --git a/doc/reference/app.md b/doc/reference/app.md index 67395eace..a6b29b370 100644 --- a/doc/reference/app.md +++ b/doc/reference/app.md @@ -40,7 +40,6 @@ instance somewhere in the DOM. is complete. The `option` object is an object with the following keys: - - **`position (string)`**: either `first-child` or `last-child`. This option determines the position of the application in the target: either first or last child. @@ -130,7 +129,7 @@ what it could look like in practice: ```js // in the main js file: -const { loadFile, mount } = owl; +const { mount } = owl; // async, so we can use async/await (async function setup() { diff --git a/doc/reference/concurrency_model.md b/doc/reference/concurrency_model.md index b219c7dc7..7c8ba8da0 100644 --- a/doc/reference/concurrency_model.md +++ b/doc/reference/concurrency_model.md @@ -72,7 +72,6 @@ component (with some code like `app.mount(document.body)`). 1. `willStart` is called on `A` 2. when it is done, template `A` is rendered. - - component `B` is created 1. `willStart` is called on `B` 2. template `B` is rendered @@ -116,7 +115,6 @@ Here is what Owl will do: 1. because of a state change, the method `render` is called on `C` 2. template `C` is rendered again - - component `D` is updated: 1. hook `willUpdateProps` is called on `D` (async) 2. template `D` is rerendered @@ -130,7 +128,6 @@ Here is what Owl will do: 4. components `F`, `D` are patched in that order 5. component `C` is patched, which will cause recursively: - 1. `willUnmount` hook on `E` 2. destruction of `E`, diff --git a/doc/reference/hooks.md b/doc/reference/hooks.md index 7ea668f1f..1ef865b5d 100644 --- a/doc/reference/hooks.md +++ b/doc/reference/hooks.md @@ -9,7 +9,7 @@ - [`useState`](#usestate) - [`useRef`](#useref) - [`useSubEnv` and `useChildSubEnv`](#usesubenv-and-usechildsubenv) - - [`useExternalListener`](#useexternallistener) + - [`useListener`](#uselistener) - [`useComponent`](#usecomponent) - [`useEnv`](#useenv) - [`useEffect`](#useeffect) @@ -187,16 +187,16 @@ frozen, to prevent unwanted modifications. Note that both these hooks can be called an arbitrary number of times. The `env` will then be updated accordingly. -### `useExternalListener` +### `useListener` -The `useExternalListener` hook helps solve a very common problem: adding and removing +The `useListener` hook helps solve a very common problem: adding and removing a listener on some target whenever a component is mounted/unmounted. It takes a target as its first argument, forwards the other arguments to `addEventListener`. For example, a dropdown menu (or its parent) may need to listen to a `click` event on `window` to be closed: ```js -useExternalListener(window, "click", this.closeMenu, { capture: true }); +useListener(window, "click", this.closeMenu, { capture: true }); ``` ### `useComponent` diff --git a/doc/reference/utils.md b/doc/reference/utils.md index dba403786..f5e9647f8 100644 --- a/doc/reference/utils.md +++ b/doc/reference/utils.md @@ -6,7 +6,6 @@ functions are all available in the `owl.utils` namespace. ## Content - [`whenReady`](#whenready): executing code when DOM is ready -- [`loadFile`](#loadfile): loading a file (useful for templates) - [`EventBus`](#eventbus): a simple EventBus - [`validate`](#validate): a validation function - [`batched`](#batched): batch function calls @@ -32,21 +31,6 @@ whenReady(function () { }); ``` -## `loadFile` - -`loadFile` is a helper function to fetch a file. It simply -performs a `GET` request and returns the resulting string in a promise. The -initial usecase for this function is to load a template file. For example: - -```js -const { loadFile } = owl; - -async function makeEnv() { - const templates = await loadFile("templates.xml"); - // do something -} -``` - ## `EventBus` It is a simple `EventBus`, with the same API as usual DOM elements, and an diff --git a/docs/owl.js b/docs/owl.js index 5e332a0ed..addbc6730 100644 --- a/docs/owl.js +++ b/docs/owl.js @@ -1,6296 +1,6709 @@ -function filterOutModifiersFromData(dataList) { - dataList = dataList.slice(); - const modifiers = []; - let elm; - while ((elm = dataList[0]) && typeof elm === "string") { - modifiers.push(dataList.shift()); - } - return { modifiers, data: dataList }; -} -const config = { - // whether or not blockdom should normalize DOM whenever a block is created. - // Normalizing dom mean removing empty text nodes (or containing only spaces) - shouldNormalizeDom: true, - // this is the main event handler. Every event handler registered with blockdom - // will go through this function, giving it the data registered in the block - // and the event - mainEventHandler: (data, ev, currentTarget) => { - if (typeof data === "function") { - data(ev); - } - else if (Array.isArray(data)) { - data = filterOutModifiersFromData(data).data; - data[0](data[1], ev); - } - return false; - }, -}; +// Custom error class that wraps error that happen in the owl lifecycle +class OwlError extends Error { + cause; +} -// ----------------------------------------------------------------------------- -// Toggler node -// ----------------------------------------------------------------------------- -class VToggler { - constructor(key, child) { - this.key = key; - this.child = child; - } - mount(parent, afterNode) { - this.parentEl = parent; - this.child.mount(parent, afterNode); - } - moveBeforeDOMNode(node, parent) { - this.child.moveBeforeDOMNode(node, parent); - } - moveBeforeVNode(other, afterNode) { - this.moveBeforeDOMNode((other && other.firstNode()) || afterNode); - } - patch(other, withBeforeRemove) { - if (this === other) { - return; - } - let child1 = this.child; - let child2 = other.child; - if (this.key === other.key) { - child1.patch(child2, withBeforeRemove); - } - else { - child2.mount(this.parentEl, child1.firstNode()); - if (withBeforeRemove) { - child1.beforeRemove(); - } - child1.remove(); - this.child = child2; - this.key = other.key; - } - } - beforeRemove() { - this.child.beforeRemove(); - } - remove() { - this.child.remove(); - } - firstNode() { - return this.child.firstNode(); - } - toString() { - return this.child.toString(); - } -} -function toggler(key, child) { - return new VToggler(key, child); +/** + * Parses an XML string into an XML document, throwing errors on parser errors + * instead of returning an XML document containing the parseerror. + * + * @param xml the string to parse + * @returns an XML document corresponding to the content of the string + */ +function parseXML(xml) { + const parser = new DOMParser(); + const doc = parser.parseFromString(xml, "text/xml"); + if (doc.getElementsByTagName("parsererror").length) { + let msg = "Invalid XML in template."; + const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent; + if (parsererrorText) { + msg += "\nThe parser has produced the following error message:\n" + parsererrorText; + const re = /\d+/g; + const firstMatch = re.exec(parsererrorText); + if (firstMatch) { + const lineNumber = Number(firstMatch[0]); + const line = xml.split("\n")[lineNumber - 1]; + const secondMatch = re.exec(parsererrorText); + if (line && secondMatch) { + const columnIndex = Number(secondMatch[0]) - 1; + if (line[columnIndex]) { + msg += + `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` + + `${line}\n${"-".repeat(columnIndex - 1)}^`; + } + } + } + } + throw new OwlError(msg); + } + return doc; } -// Custom error class that wraps error that happen in the owl lifecycle -class OwlError extends Error { +function filterOutModifiersFromData(dataList) { + dataList = dataList.slice(); + const modifiers = []; + let elm; + while ((elm = dataList[0]) && typeof elm === "string") { + modifiers.push(dataList.shift()); + } + return { modifiers, data: dataList }; } +const config$1 = { + // whether or not blockdom should normalize DOM whenever a block is created. + // Normalizing dom mean removing empty text nodes (or containing only spaces) + shouldNormalizeDom: true, + // this is the main event handler. Every event handler registered with blockdom + // will go through this function, giving it the data registered in the block + // and the event + mainEventHandler: (data, ev, currentTarget) => { + if (typeof data === "function") { + data(ev); + } + else if (Array.isArray(data)) { + data = filterOutModifiersFromData(data).data; + data[0](data[1], ev); + } + return false; + }, +}; -const { setAttribute: elemSetAttribute, removeAttribute } = Element.prototype; -const tokenList = DOMTokenList.prototype; -const tokenListAdd = tokenList.add; -const tokenListRemove = tokenList.remove; -const isArray = Array.isArray; -const { split, trim } = String.prototype; -const wordRegexp = /\s+/; -/** - * We regroup here all code related to updating attributes in a very loose sense: - * attributes, properties and classs are all managed by the functions in this - * file. - */ -function setAttribute(key, value) { - switch (value) { - case false: - case undefined: - removeAttribute.call(this, key); - break; - case true: - elemSetAttribute.call(this, key, ""); - break; - default: - elemSetAttribute.call(this, key, value); - } -} -function createAttrUpdater(attr) { - return function (value) { - setAttribute.call(this, attr, value); - }; -} -function attrsSetter(attrs) { - if (isArray(attrs)) { - if (attrs[0] === "class") { - setClass.call(this, attrs[1]); - } - else { - setAttribute.call(this, attrs[0], attrs[1]); - } - } - else { - for (let k in attrs) { - if (k === "class") { - setClass.call(this, attrs[k]); - } - else { - setAttribute.call(this, k, attrs[k]); - } - } - } -} -function attrsUpdater(attrs, oldAttrs) { - if (isArray(attrs)) { - const name = attrs[0]; - const val = attrs[1]; - if (name === oldAttrs[0]) { - if (val === oldAttrs[1]) { - return; - } - if (name === "class") { - updateClass.call(this, val, oldAttrs[1]); - } - else { - setAttribute.call(this, name, val); - } - } - else { - removeAttribute.call(this, oldAttrs[0]); - setAttribute.call(this, name, val); - } - } - else { - for (let k in oldAttrs) { - if (!(k in attrs)) { - if (k === "class") { - updateClass.call(this, "", oldAttrs[k]); - } - else { - removeAttribute.call(this, k); - } - } - } - for (let k in attrs) { - const val = attrs[k]; - if (val !== oldAttrs[k]) { - if (k === "class") { - updateClass.call(this, val, oldAttrs[k]); - } - else { - setAttribute.call(this, k, val); - } - } - } - } -} -function toClassObj(expr) { - const result = {}; - switch (typeof expr) { - case "string": - // we transform here a list of classes into an object: - // 'hey you' becomes {hey: true, you: true} - const str = trim.call(expr); - if (!str) { - return {}; - } - let words = split.call(str, wordRegexp); - for (let i = 0, l = words.length; i < l; i++) { - result[words[i]] = true; - } - return result; - case "object": - // this is already an object but we may need to split keys: - // {'a': true, 'b c': true} should become {a: true, b: true, c: true} - for (let key in expr) { - const value = expr[key]; - if (value) { - key = trim.call(key); - if (!key) { - continue; - } - const words = split.call(key, wordRegexp); - for (let word of words) { - result[word] = value; - } - } - } - return result; - case "undefined": - return {}; - case "number": - return { [expr]: true }; - default: - return { [expr]: true }; - } -} -function setClass(val) { - val = val === "" ? {} : toClassObj(val); - // add classes - const cl = this.classList; - for (let c in val) { - tokenListAdd.call(cl, c); - } -} -function updateClass(val, oldVal) { - oldVal = oldVal === "" ? {} : toClassObj(oldVal); - val = val === "" ? {} : toClassObj(val); - const cl = this.classList; - // remove classes - for (let c in oldVal) { - if (!(c in val)) { - tokenListRemove.call(cl, c); - } - } - // add classes - for (let c in val) { - if (!(c in oldVal)) { - tokenListAdd.call(cl, c); - } - } +// ----------------------------------------------------------------------------- +// Toggler node +// ----------------------------------------------------------------------------- +const txt = document.createTextNode(""); +class VToggler { + key; + child; + parentEl; + constructor(key, child) { + this.key = key; + this.child = child; + } + mount(parent, afterNode) { + this.parentEl = parent; + this.child.mount(parent, afterNode); + } + moveBeforeDOMNode(node, parent) { + this.child.moveBeforeDOMNode(node, parent); + } + moveBeforeVNode(other, afterNode) { + this.moveBeforeDOMNode((other && other.firstNode()) || afterNode); + } + patch(other, withBeforeRemove) { + if (this === other) { + return; + } + let child1 = this.child; + let child2 = other.child; + if (this.key === other.key) { + child1.patch(child2, withBeforeRemove); + } + else { + const firstNode = child1.firstNode(); + firstNode.parentElement.insertBefore(txt, firstNode); + if (withBeforeRemove) { + child1.beforeRemove(); + } + child1.remove(); + child2.mount(this.parentEl, txt); + this.child = child2; + this.key = other.key; + } + } + beforeRemove() { + this.child.beforeRemove(); + } + remove() { + this.child.remove(); + } + firstNode() { + return this.child.firstNode(); + } + toString() { + return this.child.toString(); + } +} +function toggler(key, child) { + return new VToggler(key, child); } -/** - * Creates a batched version of a callback so that all calls to it in the same - * microtick will only call the original callback once. - * - * @param callback the callback to batch - * @returns a batched version of the original callback - */ -function batched(callback) { - let scheduled = false; - return async (...args) => { - if (!scheduled) { - scheduled = true; - await Promise.resolve(); - scheduled = false; - callback(...args); - } - }; -} -/** - * Determine whether the given element is contained in its ownerDocument: - * either directly or with a shadow root in between. - */ -function inOwnerDocument(el) { - if (!el) { - return false; - } - if (el.ownerDocument.contains(el)) { - return true; - } - const rootNode = el.getRootNode(); - return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host); -} -/** - * Determine whether the given element is contained in a specific root documnet: - * either directly or with a shadow root in between or in an iframe. - */ -function isAttachedToDocument(element, documentElement) { - let current = element; - const shadowRoot = documentElement.defaultView.ShadowRoot; - while (current) { - if (current === documentElement) { - return true; - } - if (current.parentNode) { - current = current.parentNode; - } - else if (current instanceof shadowRoot && current.host) { - current = current.host; - } - else { - return false; - } - } - return false; -} -function validateTarget(target) { - // Get the document and HTMLElement corresponding to the target to allow mounting in iframes - const document = target && target.ownerDocument; - if (document) { - if (!document.defaultView) { - throw new OwlError("Cannot mount a component: the target document is not attached to a window (defaultView is missing)"); - } - const HTMLElement = document.defaultView.HTMLElement; - if (target instanceof HTMLElement || target instanceof ShadowRoot) { - if (!isAttachedToDocument(target, document)) { - throw new OwlError("Cannot mount a component on a detached dom node"); - } - return; - } - } - throw new OwlError("Cannot mount component: the target is not a valid DOM element"); -} -class EventBus extends EventTarget { - trigger(name, payload) { - this.dispatchEvent(new CustomEvent(name, { detail: payload })); - } -} -function whenReady(fn) { - return new Promise(function (resolve) { - if (document.readyState !== "loading") { - resolve(true); - } - else { - document.addEventListener("DOMContentLoaded", resolve, false); - } - }).then(fn || function () { }); -} -async function loadFile(url) { - const result = await fetch(url); - if (!result.ok) { - throw new OwlError("Error while fetching xml templates"); - } - return await result.text(); -} -/* - * This class just transports the fact that a string is safe - * to be injected as HTML. Overriding a JS primitive is quite painful though - * so we need to redfine toString and valueOf. - */ -class Markup extends String { -} -function htmlEscape(str) { - if (str instanceof Markup) { - return str; - } - if (str === undefined) { - return markup(""); - } - if (typeof str === "number") { - return markup(String(str)); - } - [ - ["&", "&"], - ["<", "<"], - [">", ">"], - ["'", "'"], - ['"', """], - ["`", "`"], - ].forEach((pairs) => { - str = String(str).replace(new RegExp(pairs[0], "g"), pairs[1]); - }); - return markup(str); -} -function markup(valueOrStrings, ...placeholders) { - if (!Array.isArray(valueOrStrings)) { - return new Markup(valueOrStrings); - } - const strings = valueOrStrings; - let acc = ""; - let i = 0; - for (; i < placeholders.length; ++i) { - acc += strings[i] + htmlEscape(placeholders[i]); - } - acc += strings[i]; - return new Markup(acc); +const { setAttribute: elemSetAttribute, removeAttribute } = Element.prototype; +const tokenList = DOMTokenList.prototype; +const tokenListAdd = tokenList.add; +const tokenListRemove = tokenList.remove; +const isArray = Array.isArray; +const { split, trim } = String.prototype; +const wordRegexp = /\s+/; +/** + * We regroup here all code related to updating attributes in a very loose sense: + * attributes, properties and classs are all managed by the functions in this + * file. + */ +function setAttribute(key, value) { + switch (value) { + case false: + case undefined: + removeAttribute.call(this, key); + break; + case true: + elemSetAttribute.call(this, key, ""); + break; + default: + elemSetAttribute.call(this, key, value); + } +} +function createAttrUpdater(attr) { + return function (value) { + setAttribute.call(this, attr, value); + }; +} +function attrsSetter(attrs) { + if (isArray(attrs)) { + if (attrs[0] === "class") { + setClass.call(this, attrs[1]); + } + else { + setAttribute.call(this, attrs[0], attrs[1]); + } + } + else { + for (let k in attrs) { + if (k === "class") { + setClass.call(this, attrs[k]); + } + else { + setAttribute.call(this, k, attrs[k]); + } + } + } +} +function attrsUpdater(attrs, oldAttrs) { + if (isArray(attrs)) { + const name = attrs[0]; + const val = attrs[1]; + if (name === oldAttrs[0]) { + if (val === oldAttrs[1]) { + return; + } + if (name === "class") { + updateClass.call(this, val, oldAttrs[1]); + } + else { + setAttribute.call(this, name, val); + } + } + else { + removeAttribute.call(this, oldAttrs[0]); + setAttribute.call(this, name, val); + } + } + else { + for (let k in oldAttrs) { + if (!(k in attrs)) { + if (k === "class") { + updateClass.call(this, "", oldAttrs[k]); + } + else { + removeAttribute.call(this, k); + } + } + } + for (let k in attrs) { + const val = attrs[k]; + if (val !== oldAttrs[k]) { + if (k === "class") { + updateClass.call(this, val, oldAttrs[k]); + } + else { + setAttribute.call(this, k, val); + } + } + } + } +} +function toClassObj(expr) { + const result = {}; + switch (typeof expr) { + case "string": + // we transform here a list of classes into an object: + // 'hey you' becomes {hey: true, you: true} + const str = trim.call(expr); + if (!str) { + return {}; + } + let words = split.call(str, wordRegexp); + for (let i = 0, l = words.length; i < l; i++) { + result[words[i]] = true; + } + return result; + case "object": + // this is already an object but we may need to split keys: + // {'a': true, 'b c': true} should become {a: true, b: true, c: true} + for (let key in expr) { + const value = expr[key]; + if (value) { + key = trim.call(key); + if (!key) { + continue; + } + const words = split.call(key, wordRegexp); + for (let word of words) { + result[word] = value; + } + } + } + return result; + case "undefined": + return {}; + case "number": + return { [expr]: true }; + default: + return { [expr]: true }; + } +} +function setClass(val) { + val = val === "" ? {} : toClassObj(val); + // add classes + const cl = this.classList; + for (let c in val) { + tokenListAdd.call(cl, c); + } +} +function updateClass(val, oldVal) { + oldVal = oldVal === "" ? {} : toClassObj(oldVal); + val = val === "" ? {} : toClassObj(val); + const cl = this.classList; + // remove classes + for (let c in oldVal) { + if (!(c in val)) { + tokenListRemove.call(cl, c); + } + } + // add classes + for (let c in val) { + if (!(c in oldVal)) { + tokenListAdd.call(cl, c); + } + } } -function createEventHandler(rawEvent) { - const eventName = rawEvent.split(".")[0]; - const capture = rawEvent.includes(".capture"); - if (rawEvent.includes(".synthetic")) { - return createSyntheticHandler(eventName, capture); - } - else { - return createElementHandler(eventName, capture); - } -} -// Native listener -let nextNativeEventId = 1; -function createElementHandler(evName, capture = false) { - let eventKey = `__event__${evName}_${nextNativeEventId++}`; - if (capture) { - eventKey = `${eventKey}_capture`; - } - function listener(ev) { - const currentTarget = ev.currentTarget; - if (!currentTarget || !inOwnerDocument(currentTarget)) - return; - const data = currentTarget[eventKey]; - if (!data) - return; - config.mainEventHandler(data, ev, currentTarget); - } - function setup(data) { - this[eventKey] = data; - this.addEventListener(evName, listener, { capture }); - } - function remove() { - delete this[eventKey]; - this.removeEventListener(evName, listener, { capture }); - } - function update(data) { - this[eventKey] = data; - } - return { setup, update, remove }; -} -// Synthetic handler: a form of event delegation that allows placing only one -// listener per event type. -let nextSyntheticEventId = 1; -function createSyntheticHandler(evName, capture = false) { - let eventKey = `__event__synthetic_${evName}`; - if (capture) { - eventKey = `${eventKey}_capture`; - } - setupSyntheticEvent(evName, eventKey, capture); - const currentId = nextSyntheticEventId++; - function setup(data) { - const _data = this[eventKey] || {}; - _data[currentId] = data; - this[eventKey] = _data; - } - function remove() { - delete this[eventKey]; - } - return { setup, update: setup, remove }; -} -function nativeToSyntheticEvent(eventKey, event) { - let dom = event.target; - while (dom !== null) { - const _data = dom[eventKey]; - if (_data) { - for (const data of Object.values(_data)) { - const stopped = config.mainEventHandler(data, event, dom); - if (stopped) - return; - } - } - dom = dom.parentNode; - } -} -const CONFIGURED_SYNTHETIC_EVENTS = {}; -function setupSyntheticEvent(evName, eventKey, capture = false) { - if (CONFIGURED_SYNTHETIC_EVENTS[eventKey]) { - return; - } - document.addEventListener(evName, (event) => nativeToSyntheticEvent(eventKey, event), { - capture, - }); - CONFIGURED_SYNTHETIC_EVENTS[eventKey] = true; +/** + * Creates a batched version of a callback so that all calls to it in the same + * microtick will only call the original callback once. + * + * @param callback the callback to batch + * @returns a batched version of the original callback + */ +function batched(callback) { + let scheduled = false; + return function batchedCall(...args) { + if (!scheduled) { + scheduled = true; + queueMicrotask(() => { + scheduled = false; + callback(...args); + }); + } + }; +} +/** + * Determine whether the given element is contained in its ownerDocument: + * either directly or with a shadow root in between. + */ +function inOwnerDocument(el) { + if (!el) { + return false; + } + if (el.ownerDocument.contains(el)) { + return true; + } + const rootNode = el.getRootNode(); + return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host); +} +/** + * Determine whether the given element is contained in a specific root documnet: + * either directly or with a shadow root in between or in an iframe. + */ +function isAttachedToDocument(element, documentElement) { + let current = element; + const shadowRoot = documentElement.defaultView.ShadowRoot; + while (current) { + if (current === documentElement) { + return true; + } + if (current.parentNode) { + current = current.parentNode; + } + else if (current instanceof shadowRoot && current.host) { + current = current.host; + } + else { + return false; + } + } + return false; +} +function validateTarget(target) { + // Get the document and HTMLElement corresponding to the target to allow mounting in iframes + const document = target && target.ownerDocument; + if (document) { + if (!document.defaultView) { + throw new OwlError("Cannot mount a component: the target document is not attached to a window (defaultView is missing)"); + } + const HTMLElement = document.defaultView.HTMLElement; + if (target instanceof HTMLElement || target instanceof ShadowRoot) { + if (!isAttachedToDocument(target, document)) { + throw new OwlError("Cannot mount a component on a detached dom node"); + } + return; + } + } + throw new OwlError("Cannot mount component: the target is not a valid DOM element"); +} +class EventBus extends EventTarget { + trigger(name, payload) { + this.dispatchEvent(new CustomEvent(name, { detail: payload })); + } +} +function whenReady(fn) { + return new Promise(function (resolve) { + if (document.readyState !== "loading") { + resolve(true); + } + else { + document.addEventListener("DOMContentLoaded", resolve, false); + } + }).then(fn || function () { }); +} +/* + * This class just transports the fact that a string is safe + * to be injected as HTML. Overriding a JS primitive is quite painful though + * so we need to redfine toString and valueOf. + */ +class Markup extends String { +} +function htmlEscape(str) { + if (str instanceof Markup) { + return str; + } + if (str === undefined) { + return markup(""); + } + if (typeof str === "number") { + return markup(String(str)); + } + [ + ["&", "&"], + ["<", "<"], + [">", ">"], + ["'", "'"], + ['"', """], + ["`", "`"], + ].forEach((pairs) => { + str = String(str).replace(new RegExp(pairs[0], "g"), pairs[1]); + }); + return markup(str); +} +function markup(valueOrStrings, ...placeholders) { + if (!Array.isArray(valueOrStrings)) { + return new Markup(valueOrStrings); + } + const strings = valueOrStrings; + let acc = ""; + let i = 0; + for (; i < placeholders.length; ++i) { + acc += strings[i] + htmlEscape(placeholders[i]); + } + acc += strings[i]; + return new Markup(acc); } -const getDescriptor$3 = (o, p) => Object.getOwnPropertyDescriptor(o, p); -const nodeProto$4 = Node.prototype; -const nodeInsertBefore$3 = nodeProto$4.insertBefore; -const nodeSetTextContent$1 = getDescriptor$3(nodeProto$4, "textContent").set; -const nodeRemoveChild$3 = nodeProto$4.removeChild; -// ----------------------------------------------------------------------------- -// Multi NODE -// ----------------------------------------------------------------------------- -class VMulti { - constructor(children) { - this.children = children; - } - mount(parent, afterNode) { - const children = this.children; - const l = children.length; - const anchors = new Array(l); - for (let i = 0; i < l; i++) { - let child = children[i]; - if (child) { - child.mount(parent, afterNode); - } - else { - const childAnchor = document.createTextNode(""); - anchors[i] = childAnchor; - nodeInsertBefore$3.call(parent, childAnchor, afterNode); - } - } - this.anchors = anchors; - this.parentEl = parent; - } - moveBeforeDOMNode(node, parent = this.parentEl) { - this.parentEl = parent; - const children = this.children; - const anchors = this.anchors; - for (let i = 0, l = children.length; i < l; i++) { - let child = children[i]; - if (child) { - child.moveBeforeDOMNode(node, parent); - } - else { - const anchor = anchors[i]; - nodeInsertBefore$3.call(parent, anchor, node); - } - } - } - moveBeforeVNode(other, afterNode) { - if (other) { - const next = other.children[0]; - afterNode = (next ? next.firstNode() : other.anchors[0]) || null; - } - const children = this.children; - const parent = this.parentEl; - const anchors = this.anchors; - for (let i = 0, l = children.length; i < l; i++) { - let child = children[i]; - if (child) { - child.moveBeforeVNode(null, afterNode); - } - else { - const anchor = anchors[i]; - nodeInsertBefore$3.call(parent, anchor, afterNode); - } - } - } - patch(other, withBeforeRemove) { - if (this === other) { - return; - } - const children1 = this.children; - const children2 = other.children; - const anchors = this.anchors; - const parentEl = this.parentEl; - for (let i = 0, l = children1.length; i < l; i++) { - const vn1 = children1[i]; - const vn2 = children2[i]; - if (vn1) { - if (vn2) { - vn1.patch(vn2, withBeforeRemove); - } - else { - const afterNode = vn1.firstNode(); - const anchor = document.createTextNode(""); - anchors[i] = anchor; - nodeInsertBefore$3.call(parentEl, anchor, afterNode); - if (withBeforeRemove) { - vn1.beforeRemove(); - } - vn1.remove(); - children1[i] = undefined; - } - } - else if (vn2) { - children1[i] = vn2; - const anchor = anchors[i]; - vn2.mount(parentEl, anchor); - nodeRemoveChild$3.call(parentEl, anchor); - } - } - } - beforeRemove() { - const children = this.children; - for (let i = 0, l = children.length; i < l; i++) { - const child = children[i]; - if (child) { - child.beforeRemove(); - } - } - } - remove() { - const parentEl = this.parentEl; - if (this.isOnlyChild) { - nodeSetTextContent$1.call(parentEl, ""); - } - else { - const children = this.children; - const anchors = this.anchors; - for (let i = 0, l = children.length; i < l; i++) { - const child = children[i]; - if (child) { - child.remove(); - } - else { - nodeRemoveChild$3.call(parentEl, anchors[i]); - } - } - } - } - firstNode() { - const child = this.children[0]; - return child ? child.firstNode() : this.anchors[0]; - } - toString() { - return this.children.map((c) => (c ? c.toString() : "")).join(""); - } -} -function multi(children) { - return new VMulti(children); +function createEventHandler(rawEvent) { + const eventName = rawEvent.split(".")[0]; + const capture = rawEvent.includes(".capture"); + const passive = rawEvent.includes(".passive"); + if (rawEvent.includes(".synthetic")) { + return createSyntheticHandler(eventName, capture, passive); + } + else { + return createElementHandler(eventName, capture, passive); + } +} +// Native listener +let nextNativeEventId = 1; +function createElementHandler(evName, capture = false, passive = false) { + let eventKey = `__event__${evName}_${nextNativeEventId++}`; + if (capture) { + eventKey = `${eventKey}_capture`; + } + function listener(ev) { + const currentTarget = ev.currentTarget; + if (!currentTarget || !inOwnerDocument(currentTarget)) + return; + const data = currentTarget[eventKey]; + if (!data) + return; + config$1.mainEventHandler(data, ev, currentTarget); + } + const options = { capture, passive }; + function setup(data) { + this[eventKey] = data; + this.addEventListener(evName, listener, options); + } + function remove() { + delete this[eventKey]; + this.removeEventListener(evName, listener, options); + } + function update(data) { + this[eventKey] = data; + } + return { setup, update, remove }; +} +// Synthetic handler: a form of event delegation that allows placing only one +// listener per event type. +let nextSyntheticEventId = 1; +function createSyntheticHandler(evName, capture = false, passive = false) { + let eventKey = `__event__synthetic_${evName}`; + if (capture) { + eventKey = `${eventKey}_capture`; + } + setupSyntheticEvent(evName, eventKey, capture, passive); + const currentId = nextSyntheticEventId++; + function setup(data) { + const _data = this[eventKey] || {}; + _data[currentId] = data; + this[eventKey] = _data; + } + function remove() { + delete this[eventKey]; + } + return { setup, update: setup, remove }; +} +function nativeToSyntheticEvent(eventKey, event) { + let dom = event.target; + while (dom !== null) { + const _data = dom[eventKey]; + if (_data) { + for (const data of Object.values(_data)) { + const stopped = config$1.mainEventHandler(data, event, dom); + if (stopped) + return; + } + } + dom = dom.parentNode; + } +} +const CONFIGURED_SYNTHETIC_EVENTS = {}; +function setupSyntheticEvent(evName, eventKey, capture = false, passive = false) { + if (CONFIGURED_SYNTHETIC_EVENTS[eventKey]) { + return; + } + document.addEventListener(evName, (event) => nativeToSyntheticEvent(eventKey, event), { + capture, + passive, + }); + CONFIGURED_SYNTHETIC_EVENTS[eventKey] = true; } -const getDescriptor$2 = (o, p) => Object.getOwnPropertyDescriptor(o, p); -const nodeProto$3 = Node.prototype; -const characterDataProto$1 = CharacterData.prototype; -const nodeInsertBefore$2 = nodeProto$3.insertBefore; -const characterDataSetData$1 = getDescriptor$2(characterDataProto$1, "data").set; -const nodeRemoveChild$2 = nodeProto$3.removeChild; -class VSimpleNode { - constructor(text) { - this.text = text; - } - mountNode(node, parent, afterNode) { - this.parentEl = parent; - nodeInsertBefore$2.call(parent, node, afterNode); - this.el = node; - } - moveBeforeDOMNode(node, parent = this.parentEl) { - this.parentEl = parent; - nodeInsertBefore$2.call(parent, this.el, node); - } - moveBeforeVNode(other, afterNode) { - nodeInsertBefore$2.call(this.parentEl, this.el, other ? other.el : afterNode); - } - beforeRemove() { } - remove() { - nodeRemoveChild$2.call(this.parentEl, this.el); - } - firstNode() { - return this.el; - } - toString() { - return this.text; - } -} -class VText$1 extends VSimpleNode { - mount(parent, afterNode) { - this.mountNode(document.createTextNode(toText(this.text)), parent, afterNode); - } - patch(other) { - const text2 = other.text; - if (this.text !== text2) { - characterDataSetData$1.call(this.el, toText(text2)); - this.text = text2; - } - } -} -class VComment extends VSimpleNode { - mount(parent, afterNode) { - this.mountNode(document.createComment(toText(this.text)), parent, afterNode); - } - patch() { } -} -function text(str) { - return new VText$1(str); -} -function comment(str) { - return new VComment(str); -} -function toText(value) { - switch (typeof value) { - case "string": - return value; - case "number": - return String(value); - case "boolean": - return value ? "true" : "false"; - default: - return value || ""; - } +const getDescriptor$3 = (o, p) => Object.getOwnPropertyDescriptor(o, p); +const nodeProto$4 = Node.prototype; +const nodeInsertBefore$3 = nodeProto$4.insertBefore; +const nodeSetTextContent$1 = getDescriptor$3(nodeProto$4, "textContent").set; +const nodeRemoveChild$3 = nodeProto$4.removeChild; +// ----------------------------------------------------------------------------- +// Multi NODE +// ----------------------------------------------------------------------------- +class VMulti { + children; + anchors; + parentEl; + isOnlyChild; + constructor(children) { + this.children = children; + } + mount(parent, afterNode) { + const children = this.children; + const l = children.length; + const anchors = new Array(l); + for (let i = 0; i < l; i++) { + let child = children[i]; + if (child) { + child.mount(parent, afterNode); + } + else { + const childAnchor = document.createTextNode(""); + anchors[i] = childAnchor; + nodeInsertBefore$3.call(parent, childAnchor, afterNode); + } + } + this.anchors = anchors; + this.parentEl = parent; + } + moveBeforeDOMNode(node, parent = this.parentEl) { + this.parentEl = parent; + const children = this.children; + const anchors = this.anchors; + for (let i = 0, l = children.length; i < l; i++) { + let child = children[i]; + if (child) { + child.moveBeforeDOMNode(node, parent); + } + else { + const anchor = anchors[i]; + nodeInsertBefore$3.call(parent, anchor, node); + } + } + } + moveBeforeVNode(other, afterNode) { + if (other) { + const next = other.children[0]; + afterNode = (next ? next.firstNode() : other.anchors[0]) || null; + } + const children = this.children; + const parent = this.parentEl; + const anchors = this.anchors; + for (let i = 0, l = children.length; i < l; i++) { + let child = children[i]; + if (child) { + child.moveBeforeVNode(null, afterNode); + } + else { + const anchor = anchors[i]; + nodeInsertBefore$3.call(parent, anchor, afterNode); + } + } + } + patch(other, withBeforeRemove) { + if (this === other) { + return; + } + const children1 = this.children; + const children2 = other.children; + const anchors = this.anchors; + const parentEl = this.parentEl; + for (let i = 0, l = children1.length; i < l; i++) { + const vn1 = children1[i]; + const vn2 = children2[i]; + if (vn1) { + if (vn2) { + vn1.patch(vn2, withBeforeRemove); + } + else { + const afterNode = vn1.firstNode(); + const anchor = document.createTextNode(""); + anchors[i] = anchor; + nodeInsertBefore$3.call(parentEl, anchor, afterNode); + if (withBeforeRemove) { + vn1.beforeRemove(); + } + vn1.remove(); + children1[i] = undefined; + } + } + else if (vn2) { + children1[i] = vn2; + const anchor = anchors[i]; + vn2.mount(parentEl, anchor); + nodeRemoveChild$3.call(parentEl, anchor); + } + } + } + beforeRemove() { + const children = this.children; + for (let i = 0, l = children.length; i < l; i++) { + const child = children[i]; + if (child) { + child.beforeRemove(); + } + } + } + remove() { + const parentEl = this.parentEl; + if (this.isOnlyChild) { + nodeSetTextContent$1.call(parentEl, ""); + } + else { + const children = this.children; + const anchors = this.anchors; + for (let i = 0, l = children.length; i < l; i++) { + const child = children[i]; + if (child) { + child.remove(); + } + else { + nodeRemoveChild$3.call(parentEl, anchors[i]); + } + } + } + } + firstNode() { + const child = this.children[0]; + return child ? child.firstNode() : this.anchors[0]; + } + toString() { + return this.children.map((c) => (c ? c.toString() : "")).join(""); + } +} +function multi(children) { + return new VMulti(children); } -const getDescriptor$1 = (o, p) => Object.getOwnPropertyDescriptor(o, p); -const nodeProto$2 = Node.prototype; -const elementProto = Element.prototype; -const characterDataProto = CharacterData.prototype; -const characterDataSetData = getDescriptor$1(characterDataProto, "data").set; -const nodeGetFirstChild = getDescriptor$1(nodeProto$2, "firstChild").get; -const nodeGetNextSibling = getDescriptor$1(nodeProto$2, "nextSibling").get; -const NO_OP$1 = () => { }; -function makePropSetter(name) { - return function setProp(value) { - // support 0, fallback to empty string for other falsy values - this[name] = value === 0 ? 0 : value ? value.valueOf() : ""; - }; -} -const cache$1 = {}; -/** - * Compiling blocks is a multi-step process: - * - * 1. build an IntermediateTree from the HTML element. This intermediate tree - * is a binary tree structure that encode dynamic info sub nodes, and the - * path required to reach them - * 2. process the tree to build a block context, which is an object that aggregate - * all dynamic info in a list, and also, all ref indexes. - * 3. process the context to build appropriate builder/setter functions - * 4. make a dynamic block class, which will efficiently collect references and - * create/update dynamic locations/children - * - * @param str - * @returns a new block type, that can build concrete blocks - */ -function createBlock(str) { - if (str in cache$1) { - return cache$1[str]; - } - // step 0: prepare html base element - const doc = new DOMParser().parseFromString(`${str}`, "text/xml"); - const node = doc.firstChild.firstChild; - if (config.shouldNormalizeDom) { - normalizeNode(node); - } - // step 1: prepare intermediate tree - const tree = buildTree(node); - // step 2: prepare block context - const context = buildContext(tree); - // step 3: build the final block class - const template = tree.el; - const Block = buildBlock(template, context); - cache$1[str] = Block; - return Block; -} -// ----------------------------------------------------------------------------- -// Helper -// ----------------------------------------------------------------------------- -function normalizeNode(node) { - if (node.nodeType === Node.TEXT_NODE) { - if (!/\S/.test(node.textContent)) { - node.remove(); - return; - } - } - if (node.nodeType === Node.ELEMENT_NODE) { - if (node.tagName === "pre") { - return; - } - } - for (let i = node.childNodes.length - 1; i >= 0; --i) { - normalizeNode(node.childNodes.item(i)); - } -} -function buildTree(node, parent = null, domParentTree = null) { - switch (node.nodeType) { - case Node.ELEMENT_NODE: { - // HTMLElement - let currentNS = domParentTree && domParentTree.currentNS; - const tagName = node.tagName; - let el = undefined; - const info = []; - if (tagName.startsWith("block-text-")) { - const index = parseInt(tagName.slice(11), 10); - info.push({ type: "text", idx: index }); - el = document.createTextNode(""); - } - if (tagName.startsWith("block-child-")) { - if (!domParentTree.isRef) { - addRef(domParentTree); - } - const index = parseInt(tagName.slice(12), 10); - info.push({ type: "child", idx: index }); - el = document.createTextNode(""); - } - currentNS || (currentNS = node.namespaceURI); - if (!el) { - el = currentNS - ? document.createElementNS(currentNS, tagName) - : document.createElement(tagName); - } - if (el instanceof Element) { - if (!domParentTree) { - // some html elements may have side effects when setting their attributes. - // For example, setting the src attribute of an will trigger a - // request to get the corresponding image. This is something that we - // don't want at compile time. We avoid that by putting the content of - // the block in a