mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
223 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d381e85d94 | |||
| af7520d869 | |||
| 0095bfa61f | |||
| 1b12cf9b91 | |||
| dbfc7e4acd | |||
| e838e879c0 | |||
| ee63f6bb0f | |||
| 16bbb8bc9f | |||
| 4a5db0c283 | |||
| cc50a2e3bb | |||
| 1e2b204fdd | |||
| 4a889b7b6b | |||
| 34695883c2 | |||
| 3d2e2a1873 | |||
| af6aca83a2 | |||
| 8c8ffb6a6b | |||
| 63a8fcd7e2 | |||
| e6a5934162 | |||
| 5524c2e323 | |||
| 35c1de26b8 | |||
| d1cf6b1b8d | |||
| bfa2c681cd | |||
| 609c108607 | |||
| a56939f13a | |||
| 5ff501f8b8 | |||
| 60917bb102 | |||
| b201ef8162 | |||
| 9cacc78ad7 | |||
| 0d933b8570 | |||
| 72f6daa695 | |||
| 567d78a9ac | |||
| f013c57050 | |||
| b9fae87273 | |||
| 1f0457f69b | |||
| c481a73a76 | |||
| e64e415b3b | |||
| aa406b53e2 | |||
| 57027bd0fa | |||
| 671c662fee | |||
| 0ae70b1d63 | |||
| 2701a7d861 | |||
| 3ebe985c3c | |||
| 1c0d4b5832 | |||
| 2840794fa2 | |||
| 710f42d4e4 | |||
| 45a2b0122d | |||
| 4c41f62364 | |||
| c2ab9774fb | |||
| 22e48e3be0 | |||
| 1ff3c32fe4 | |||
| 9a3e1ed99e | |||
| 438e2fbc2e | |||
| b3db085745 | |||
| b948c59c6d | |||
| 6426fe98f9 | |||
| 4dd9de349e | |||
| e1bcea77b5 | |||
| 2a19aeb6a3 | |||
| b53a34b7d1 | |||
| f881640d3d | |||
| 54c3dd76e4 | |||
| 7b887447da | |||
| 438b21df3f | |||
| d1094f647a | |||
| 49baf0de6e | |||
| b14d069ee7 | |||
| e14a4fe338 | |||
| 1f9ec46236 | |||
| 1464a3631b | |||
| 7d3c374b78 | |||
| 1dd257f33c | |||
| 1206afe6be | |||
| 5842ed51b0 | |||
| d22219f084 | |||
| 0be1a54e03 | |||
| 32921ecc20 | |||
| 1c290c1172 | |||
| 2f9d7ea58f | |||
| fe23e76341 | |||
| 73023d4869 | |||
| ff51443b8e | |||
| f8e07f6d30 | |||
| 00da778753 | |||
| 187eb922c0 | |||
| 466c12a0e6 | |||
| 8dc3ec94bf | |||
| a554436d03 | |||
| 34b5049dee | |||
| 0eccfd08fe | |||
| 98b063fd70 | |||
| e9ba94ae55 | |||
| 4d72a4f240 | |||
| a08deb895d | |||
| 60cea14b01 | |||
| f60904b740 | |||
| 376f801541 | |||
| 26f14180e2 | |||
| bced9f3d04 | |||
| 94208d9cbb | |||
| 681983142d | |||
| a525b42537 | |||
| a4cc085b6d | |||
| 837dacf1cb | |||
| 7cdd1aa9a1 | |||
| 24c569e6d5 | |||
| 050694414c | |||
| 639908366a | |||
| 59f401303a | |||
| 649e20b956 | |||
| c2280ff1bb | |||
| 8a32d823cd | |||
| 13b31458da | |||
| 92b3ab32e2 | |||
| 4ca0a157e3 | |||
| 1dec8df715 | |||
| 8f29cfc996 | |||
| da275e74fd | |||
| 3d9f4e028c | |||
| 13577997bc | |||
| 671965026d | |||
| bc7fe9023d | |||
| c2c7997df3 | |||
| a1d7e6c987 | |||
| a1e982aba5 | |||
| 2ce027a0a6 | |||
| 61f21511a5 | |||
| 5682577870 | |||
| 25b633cc63 | |||
| f16b267931 | |||
| 30ec952588 | |||
| 46ba77d0cc | |||
| 042e376c2b | |||
| b9f1af2ad5 | |||
| 7df0fdcc0a | |||
| 73d743a1c3 | |||
| 8758dfcedf | |||
| 0a10bbc023 | |||
| 9c1d369e94 | |||
| 9556696fa8 | |||
| 04eb720619 | |||
| 8e098e8ed6 | |||
| c836cead58 | |||
| 361ecbe90b | |||
| ae8d7d3be2 | |||
| ed4f92211c | |||
| f8804d6501 | |||
| de7c680387 | |||
| 6dd7391cdc | |||
| dfa7d5932d | |||
| 53911de00a | |||
| 1760d0d336 | |||
| 0aba9748c9 | |||
| 8ee0973dab | |||
| e04c5b1c6b | |||
| f52a15d7f6 | |||
| 147f6fced7 | |||
| d928927da8 | |||
| 29cc94d567 | |||
| 7f33f9b68e | |||
| 80ffa37773 | |||
| 2f70a7a352 | |||
| f7f8c34e1c | |||
| edc7d2b635 | |||
| 7f0b236747 | |||
| b7860a95b0 | |||
| f7495dc897 | |||
| ba506be58d | |||
| 91896942dd | |||
| dfab8c7333 | |||
| bd29e4f761 | |||
| b9591cfecd | |||
| 51320204f9 | |||
| ec0bafaa8e | |||
| eb508b7660 | |||
| 7c8fc323e1 | |||
| ff2393f5df | |||
| ab2a98426e | |||
| 84b3cc42cf | |||
| d28b854e8d | |||
| 6c8c69affa | |||
| faec7d4ad9 | |||
| 5b9abb6785 | |||
| eb4ca0a8d5 | |||
| 641adc6e5c | |||
| 10ab97e3c6 | |||
| e060130655 | |||
| 4e982f5460 | |||
| d3197e9865 | |||
| 0da44033ac | |||
| 6350d9598d | |||
| 9df6c0aede | |||
| 896efca43f | |||
| efe82b6cd0 | |||
| eb910193d7 | |||
| 40e486a9eb | |||
| e56580efb2 | |||
| 46e05f1c96 | |||
| 4a748d2ea4 | |||
| 8f388e712e | |||
| 0a5b6aa0ec | |||
| fc1ea10243 | |||
| d52177560b | |||
| 1701d5aa86 | |||
| b08e876136 | |||
| 005b727328 | |||
| b44f274e37 | |||
| b6bd0f92a7 | |||
| 78108e8d2c | |||
| c405dadc1b | |||
| 2bd0719b9a | |||
| 9544034e6f | |||
| cf47f8459a | |||
| 8f7d06a5bd | |||
| d1708fc252 | |||
| f23a26426d | |||
| 40613e0a76 | |||
| aa11229641 | |||
| 754272b436 | |||
| d509373228 | |||
| 379cd2a90f | |||
| 4c2115d365 | |||
| 94b48cb389 | |||
| 3ecb772a56 |
@@ -1,5 +1,6 @@
|
||||
/node_modules
|
||||
/dist
|
||||
|
||||
npm-debug.log
|
||||
|
||||
# misc
|
||||
@@ -14,5 +15,12 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
package-lock.json
|
||||
|
||||
#ide's
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
node_modules
|
||||
|
||||
# Extras temp file
|
||||
/tools/owl.js
|
||||
@@ -1,12 +1,9 @@
|
||||
|
||||
For copyright information, please see the COPYRIGHT file.
|
||||
|
||||
Odoo is published under the GNU LESSER GENERAL PUBLIC LICENSE, Version 3
|
||||
OWL is published under the GNU LESSER GENERAL PUBLIC LICENSE, Version 3
|
||||
(LGPLv3), as included below. Since the LGPL is a set of additional
|
||||
permissions on top of the GPL, the text of the GPL is included at the
|
||||
bottom as well.
|
||||
|
||||
Some external libraries and contributions bundled with Odoo may be published
|
||||
Some external libraries and contributions bundled with OWL may be published
|
||||
under other GPL-compatible licenses. For these, please refer to the relevant
|
||||
source files and/or license files, in the source code tree.
|
||||
|
||||
|
||||
@@ -1,65 +1,83 @@
|
||||
<h1 align="center">🦉 Odoo Web Library 🦉</h1>
|
||||
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Odoo Web Library</a> 🦉</h1>
|
||||
|
||||
_A web framework for structured, dynamic and maintainable applications_
|
||||
|
||||
## Project Overview
|
||||
|
||||
The Odoo Web Library (OWL) is a small
|
||||
The Odoo Web Library (OWL) is a small
|
||||
UI framework intended to be the basis for the [Odoo](https://www.odoo.com/) Web Client, and hopefully many
|
||||
other Odoo related projects. OWL's two key features are:
|
||||
other Odoo related projects. OWL's main feature is a _declarative component system_, with QWeb as a template engine, asynchronous rendering, and an underlying virtual dom.
|
||||
|
||||
- a declarative component system, with QWeb as a template engine, asynchronous rendering, and an underlying virtual dom,
|
||||
- and a store (state management solution, loosely inspired by VueX and React/Redux).
|
||||
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the OWL framework.
|
||||
|
||||
If you are interested, you can find a discussion on what makes OWL different
|
||||
from React and Vue [here](doc/comparison.md)
|
||||
## OWL's Design Principles
|
||||
|
||||
## Try it online!
|
||||
OWL is designed to be used in highly dynamic applications where changing
|
||||
requirements are common, code needs to be maintained by large teams.
|
||||
|
||||
An online playground is available at [https://odoo.github.io/owl/](https://odoo.github.io/owl/) to let you experiment with the OWL framework.
|
||||
- **XML based**: templates are based on the XML format, which allows interesting
|
||||
applications. For example, they could be stored in a database and modified
|
||||
dynamically with `xpaths`.
|
||||
- **templates compilation in the browser**: this may not be a good fit for all
|
||||
applications, but if you need to generate dynamically user interfaces in the
|
||||
browser, this is very powerful. For example, a generic form view component
|
||||
could generate a specific form user interface for each various models, from a XML view.
|
||||
- **no toolchain required**: this is extremely useful for some applications, if,
|
||||
for various reasons (security/deployment/dynamic modules/specific assets tools),
|
||||
it is not ok to use standard web tools based on `npm`.
|
||||
|
||||
# Example
|
||||
Owl is not designed to be fast or small (even though it is quite good on those
|
||||
two topics). If you are interested in a comparison with React or Vue, you will
|
||||
find some more information [here](doc/comparison.md).
|
||||
|
||||
Here is a short example to illustrate interactive widgets:
|
||||
## Example
|
||||
|
||||
Here is a short example to illustrate interactive components:
|
||||
|
||||
```xml
|
||||
<templates>
|
||||
<button t-name="Counter" t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>
|
||||
</templates>
|
||||
```
|
||||
|
||||
```javascript
|
||||
class ClickCounter extends owl.Component {
|
||||
inlineTemplate = `
|
||||
<button t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
this.state = { value: 0 };
|
||||
}
|
||||
class Counter extends owl.Component {
|
||||
state = { value: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
|
||||
const qweb = new owl.QWeb();
|
||||
const counter = new ClickCounter({qweb});
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const counter = new Counter({ qweb });
|
||||
counter.mount(document.body);
|
||||
```
|
||||
|
||||
More interesting examples can be found on the [playground](https://odoo.github.io/owl/) application.
|
||||
Note that we assume here that the xml templates are available in the `TEMPLATES`
|
||||
string. More interesting examples can be found on the
|
||||
[playground](https://odoo.github.io/owl/playground) application.
|
||||
|
||||
## Installing/Building
|
||||
|
||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||
|
||||
- [owl-0.8.0.js](https://odoo.github.io/owl/releases/owl-0.8.0.js)
|
||||
- [owl-0.8.0.min.js](https://odoo.github.io/owl/releases/owl-0.8.0.min.js)
|
||||
- [owl-0.16.0.js](https://github.com/odoo/owl/releases/download/v0.16.0/owl.js)
|
||||
- [owl-0.16.0.min.js](https://github.com/odoo/owl/releases/download/v0.16.0/owl.min.js)
|
||||
|
||||
Some npm scripts are available:
|
||||
|
||||
| Command | Description |
|
||||
| -------------------- | -------------------------------------------------- |
|
||||
| `npm install` | install every dependency required for this project |
|
||||
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
|
||||
| `npm run minify` | minify the prebuilt owl.js file |
|
||||
| `npm run test` | run all tests |
|
||||
| `npm run test:watch` | run all tests, and keep a watcher |
|
||||
| Command | Description |
|
||||
| --------------------- | ---------------------------------------------------------------------------- |
|
||||
| `npm install` | install every dependency required for this project |
|
||||
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
|
||||
| `npm run minify` | minify the prebuilt owl.js file |
|
||||
| `npm run test` | run all tests |
|
||||
| `npm run test:watch` | run all tests, and keep a watcher |
|
||||
| `npm run tools` | build tools applications, start a static server (see [here](doc/tooling.md)) |
|
||||
| `npm run tools:watch` | same as `tools`, but with a watcher to rebuild owl |
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -67,7 +85,10 @@ The complete documentation can be found [here](doc/readme.md). The most importan
|
||||
|
||||
- [Quick Start](doc/quick_start.md)
|
||||
- [Component](doc/component.md)
|
||||
- [Store](doc/store.md)
|
||||
- [QWeb](doc/qweb.md)
|
||||
|
||||
Found an issue in the documentation? A broken link? Some outdated information?
|
||||
Submit a PR!
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# 🦉 Animations 🦉
|
||||
|
||||
Animation is a complex topic. There are many different use cases, and many
|
||||
solutions and technologies. Owl only supports some basic use cases.
|
||||
|
||||
## Simple CSS effects
|
||||
|
||||
Sometimes, using pure CSS is enough. For these use cases, Owl is not really
|
||||
necessary: it just needs to render a DOM element with a specific class. For
|
||||
example:
|
||||
|
||||
```xml
|
||||
<a class="btn flash" t-on-click="doSomething">Click</a>
|
||||
```
|
||||
|
||||
with the following CSS:
|
||||
|
||||
```css
|
||||
btn {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.flash {
|
||||
transition: background 0.5s;
|
||||
}
|
||||
|
||||
.flash:active {
|
||||
background-color: #41454a;
|
||||
transition: background 0s;
|
||||
}
|
||||
```
|
||||
|
||||
will produce a nice flash effect whenever the user click (or activate with the
|
||||
keyboard) the button.
|
||||
|
||||
## CSS Transitions
|
||||
|
||||
A more complex situation occurs when we want to transition an element in or out
|
||||
of the page. For example, we may want a fade-in and fade-out effect.
|
||||
|
||||
The `t-transition` directive is here to help us. It works on html elements and
|
||||
on components, by adding and removing some css classes.
|
||||
|
||||
To perform useful transition effects, whenever an element appears or disappears,
|
||||
it is necessary to add/remove some css style or class at some precise moment in
|
||||
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
|
||||
directive is there to help.
|
||||
|
||||
Whenever a node has a `t-transition` directive, with a `name` value, the following
|
||||
will happen:
|
||||
|
||||
At node insertion:
|
||||
|
||||
- the css classes `name-enter` and `name-enter-active` will be added directly
|
||||
when the node is inserted into the DOM,
|
||||
- on the next animation frame: the css class `name-enter` will be removed and the
|
||||
class `name-enter-to` will be added (so they can be used to trigger css
|
||||
transition effects),
|
||||
- the css class `name-enter-active` will be removed whenever a css transition
|
||||
ends.
|
||||
|
||||
At node destruction:
|
||||
|
||||
- the css classes `name-leave` and `name-leave-active` will be added before the
|
||||
node is removed to the DOM,
|
||||
- the css class `name-leave` will be removed on the next animation frame (so it
|
||||
can be used to trigger css transition effects),
|
||||
- the css class `name-leave-active` will be removed whenever a css transition
|
||||
ends. Only then will the element be removed from the DOM.
|
||||
|
||||
For example, a simple fade in/out effect can be done with this:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```css
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
```
|
||||
|
||||
The `t-transition` directive can be applied on a node element or on a component.
|
||||
|
||||
Notes:
|
||||
|
||||
- more information on animations are available [here](animations.md).
|
||||
- Owl does not support more than one transition on a single node, so the
|
||||
`t-transition` expression must be a single value (i.e. no space allowed)
|
||||
+12
-10
@@ -7,6 +7,8 @@ In this page, we try to highlight some of these differences. Obviously, some
|
||||
effort was done to be fair. However, if you disagree with some of the points
|
||||
discussed, feel free to open an issue/submit a PR to correct this text.
|
||||
|
||||
## Content
|
||||
|
||||
- [Size](#size)
|
||||
- [Tooling/Build Step](#toolingbuild-step)
|
||||
- [Templating](#templating)
|
||||
@@ -14,7 +16,7 @@ discussed, feel free to open an issue/submit a PR to correct this text.
|
||||
- [Reactiveness](#reactiveness)
|
||||
- [State Management](#state-management)
|
||||
|
||||
### Size
|
||||
## Size
|
||||
|
||||
OWL is intended to be small and to work at a slightly lower level of abstraction
|
||||
than React and Vue. Also, jQuery is not the same kind of framework, but it is interesting to compare.
|
||||
@@ -26,7 +28,7 @@ than React and Vue. Also, jQuery is not the same kind of framework, but it is in
|
||||
| React + ReactDOM + Redux | | 40kb |
|
||||
| jQuery | 86kb | 30kb |
|
||||
|
||||
### Tooling/Build step
|
||||
## Tooling/Build step
|
||||
|
||||
OWL is designed to be easy to use in a standalone way. For various reasons,
|
||||
Odoo does not want to rely on standard web tools (such as webpack), and OWL can
|
||||
@@ -84,22 +86,22 @@ This has the advantage of having the full power of Javascript, but is less
|
||||
structured than a template language. Note that the tooling is quite impressive:
|
||||
there is a syntax highlighter for jsx here on github!
|
||||
|
||||
### Asynchronous rendering
|
||||
## Asynchronous Rendering
|
||||
|
||||
This is actually a big difference between OWL and React/Vue: components in OWL
|
||||
are totally asynchronous. They have two asynchronous hooks in their lifecycle:
|
||||
|
||||
- `willStart` (before the widget starts rendering)
|
||||
- `willStart` (before the component starts rendering)
|
||||
- `willUpdateProps` (before new props are set)
|
||||
|
||||
Both these methods can be implemented and return a promise. The rendering will
|
||||
then wait for these promises to be completed before patching the DOM. This is
|
||||
useful for some use cases: for example, a widget may want to fetch an external
|
||||
library (a calendar widget may need a specialized calendar rendering library),
|
||||
useful for some use cases: for example, a component may want to fetch an external
|
||||
library (a calendar component may need a specialized calendar rendering library),
|
||||
in its willStart hook.
|
||||
|
||||
```javascript
|
||||
class MyCalendarWidget extends owl.Component {
|
||||
class MyCalendarComponent extends owl.Component {
|
||||
...
|
||||
|
||||
willStart() {
|
||||
@@ -115,7 +117,7 @@ extremely powerful as well, as demonstrated by the Odoo Web Client.
|
||||
Lazy loading static libraries can obviously be done with React/Vue, but it is
|
||||
more convoluted.
|
||||
|
||||
### Reactiveness
|
||||
## Reactiveness
|
||||
|
||||
React has a simple model: whenever the state changes, it is
|
||||
replaced with a new state (via the setState method). Then, the DOM is patched.
|
||||
@@ -129,7 +131,7 @@ Owl is closer to vue: it also tracks magically the state properties, but it does
|
||||
only increment a counter whenever it changes (and a _deep_ counter for each of
|
||||
its parents). This assumes that the state is actually a tree.
|
||||
|
||||
### State Management
|
||||
## State Management
|
||||
|
||||
Managing the state of an application is a tricky issue. Many solutions have
|
||||
been proposed these last few years. It also depends on the kind of application we
|
||||
@@ -190,5 +192,5 @@ keeps track of who get data, and retrigger a render when it was changed.
|
||||
|
||||
Owl store is a little bit like a mix of redux and vuex: it has mutations and
|
||||
actions, like VueX, it keeps track of the state changes, but it does not notify
|
||||
a component when the state changes. Instead, components need to connect to the
|
||||
a component when the state changes. Instead, components need to connect to the
|
||||
store like in redux, with a function that will listen to the relevant state.
|
||||
|
||||
+930
-60
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
# 🦉 Event Bus 🦉
|
||||
|
||||
It is sometimes useful to use a `Bus` to communicate informations between various
|
||||
parts of the code. Owl has a very simple bus class, which manages subscriptions,
|
||||
triggering events, and callbacks.
|
||||
|
||||
```js
|
||||
const bus = new owl.EventBus();
|
||||
|
||||
bus.on("some-event", null, function(...args) {
|
||||
console.log(...args);
|
||||
});
|
||||
|
||||
bus.trigger("some-event", 1, 2, 3);
|
||||
// [1,2,3] will be logged to the console
|
||||
```
|
||||
|
||||
Its API is:
|
||||
|
||||
| Method | Description |
|
||||
| -------------------------------- | --------------------------------- |
|
||||
| `on(eventType, owner, callback)` | add a listener |
|
||||
| `off(eventType, owner)` | remove all listeners for an owner |
|
||||
| `trigger(eventType, ...args)` | trigger an event |
|
||||
| `clear` | remove all subscriptions |
|
||||
|
||||
Note that the [`Store`](store.md) is an example of an `EventBus`.
|
||||
+49
-1
@@ -1,2 +1,50 @@
|
||||
# Observer
|
||||
# 🦉 Observer 🦉
|
||||
|
||||
Owl need to be able to react to state changes. For example, whenever the state
|
||||
of a component is changed, we need to rerender it. To help with that, we have
|
||||
an Observer class. Its job is to observe some object state, and react to any
|
||||
change. To do that, it recursively replace all keys of the observed state by
|
||||
getters and setters.
|
||||
|
||||
For example, this code will display `update` in the console:
|
||||
|
||||
```javascript
|
||||
const observer = new owl.Observer();
|
||||
observer.notifyCB = () => console.log("update");
|
||||
observer.observe(obj);
|
||||
|
||||
const obj = { a: { b: 1 } };
|
||||
obj.a.b = 2;
|
||||
```
|
||||
|
||||
## Technical Limitations
|
||||
|
||||
Since the observer uses getters and setters, it is actually unable to react to
|
||||
changes in three situations:
|
||||
|
||||
- adding a key to an object
|
||||
- deleting a key from an object
|
||||
- modifying an array by setting a new value at a given index
|
||||
|
||||
In those situations, we need a way to tell the observer that something happened.
|
||||
This can be done by using the `set` and `delete` (only for objects) static
|
||||
methods of the `Observer`.
|
||||
|
||||
```javascript
|
||||
const observer = new owl.Observer();
|
||||
const obj = { a: 1 };
|
||||
observer.observe(obj);
|
||||
obj.b = 2; // won't notify the change
|
||||
owl.Observer.set(obj, "b", 2); // will notify the change
|
||||
|
||||
delete obj.b; // won't notify the change
|
||||
owl.Observer.delete(obj, "b"); // will notify the change
|
||||
```
|
||||
|
||||
```javascript
|
||||
const observer = new owl.Observer();
|
||||
const arr = ["a"];
|
||||
observer.observe(arr);
|
||||
arr[0] = "b"; // won't notify the change
|
||||
owl.Observer.set(arr, 0, "b"); // will notify the change
|
||||
```
|
||||
|
||||
+86
-46
@@ -1,61 +1,101 @@
|
||||
# Quick Start
|
||||
# 🦉 Quick Start 🦉
|
||||
|
||||
## Static Server
|
||||
|
||||
Let us assume that we have a static server running somewhere. We could then
|
||||
simply add an html page with a few extra files.
|
||||
|
||||
### HTML and CSS
|
||||
|
||||
In a file `index.html`:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>My OWL App</title>
|
||||
<link href="app.css" rel="stylesheet" />
|
||||
<script src="owl-X.Y.Z.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
<script src="app.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
In `app.css`:
|
||||
|
||||
```css
|
||||
button {
|
||||
color: darkred;
|
||||
font-size: 30px;
|
||||
width: 220px;
|
||||
}
|
||||
```
|
||||
|
||||
Also, let's not forget to add a release of OWL (`owl-X.Y.Z.js`)
|
||||
|
||||
### XML
|
||||
|
||||
In `templates.xml`:
|
||||
|
||||
```xml
|
||||
<templates>
|
||||
<button t-name="clickcounter" t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>
|
||||
</templates>
|
||||
```
|
||||
|
||||
### JS
|
||||
|
||||
To build an application (or a sub-part of an application), we need two things:
|
||||
|
||||
- an environment
|
||||
- a root widget
|
||||
- an environment: it is the global context in which we are working. It needs to
|
||||
contain a QWeb instance (preloaded with templates), and anything else that we
|
||||
need. In practice, it could context some user session information, some
|
||||
configuration keys (for example, isMobile = true/false if we are in mobile mode).
|
||||
|
||||
Here are a few steps that may be useful to get started:
|
||||
- a description of the user interface: there should be a root component, which can
|
||||
have sub components
|
||||
|
||||
Here are a few steps that we may take to get started:
|
||||
|
||||
- get the templates
|
||||
- create a qweb engine, with the templates
|
||||
- create an environment
|
||||
- create an instance of the root widget
|
||||
- mount the root widget to a DOM element
|
||||
- create an instance of the root component
|
||||
- mount the root component to a DOM element
|
||||
|
||||
## Simple example
|
||||
|
||||
In a oversimplified example, here is what it could look like:
|
||||
Let us now add the javascript to make it work, in `app.js`:
|
||||
|
||||
```javascript
|
||||
const templates = await loadTemplates();
|
||||
class ClickCounter extends owl.Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.template = "clickcounter";
|
||||
this.state = { value: 0 };
|
||||
}
|
||||
|
||||
const qweb = new QWeb();
|
||||
qweb.loadTemplates(templates);
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
|
||||
const env = {
|
||||
qweb: qweb
|
||||
};
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const counter = new ClickCounter(env);
|
||||
const target = document.getElementById("main");
|
||||
await counter.mount(target);
|
||||
}
|
||||
|
||||
const root = new RootWidget(env, { initialState: 17 });
|
||||
|
||||
const target = document.getElementById("app");
|
||||
root.mount(target);
|
||||
```
|
||||
|
||||
## Managing state
|
||||
|
||||
The environment is propagated to each subchildren. So, it is convenient to use
|
||||
to give a reference to some global object. This is potentially very useful
|
||||
in a redux-like architecture:
|
||||
|
||||
```javascript
|
||||
...
|
||||
|
||||
const store = new Store();
|
||||
|
||||
const env = {
|
||||
qweb: qweb,
|
||||
store: store,
|
||||
};
|
||||
|
||||
const state = store.getState()
|
||||
const root = new RootWidget(env, state);
|
||||
|
||||
store.on('state_change', nextState => {
|
||||
root.updateProps(nextState);
|
||||
});
|
||||
|
||||
const target = document.getElementById('app');
|
||||
root.mount(target);
|
||||
start();
|
||||
```
|
||||
|
||||
+497
-18
@@ -1,35 +1,514 @@
|
||||
# QWeb
|
||||
# 🦉 QWeb 🦉
|
||||
|
||||
QWeb is a template specification. The QWeb class in this repository is:
|
||||
## Content
|
||||
|
||||
- an implementation of the QWeb specification
|
||||
- which outputs a virtual dom instead of a string
|
||||
- and extended with a few extra directives
|
||||
- [Overview](#overview)
|
||||
- [Directives](#directives)
|
||||
- [QWeb Engine](#qweb-engine)
|
||||
- [Reference](#reference)
|
||||
- [White Spaces](#white-spaces)
|
||||
- [Root Nodes](#root-nodes)
|
||||
- [Expression Evaluation](#expression-evaluation)
|
||||
- [Static html Nodes](#static-html-nodes)
|
||||
- [Outputting Data](#outputting-data)
|
||||
- [Setting Variables](#setting-variables)
|
||||
- [Conditionals](#conditionals)
|
||||
- [Dynamic Attributes](#dynamic-attributes)
|
||||
- [Loops](#loops)
|
||||
- [Rendering Sub Templates](#rendering-sub-templates)
|
||||
- [Debugging](#debugging)
|
||||
|
||||
## QWeb Specification
|
||||
## Overview
|
||||
|
||||
add here a full description of what QWeb is supposed to be, with some examples
|
||||
[QWeb](https://www.odoo.com/documentation/12.0/reference/qweb.html) is the primary templating engine used by Odoo. It is based on the XML format, and used
|
||||
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
|
||||
generate a virtual dom representation of the HTML.
|
||||
|
||||
## QWeb Implementation
|
||||
Template directives are specified as XML attributes prefixed with `t-`, for instance `t-if` for conditionals, with elements and other attributes being rendered directly.
|
||||
|
||||
```javascript
|
||||
var qweb = new QWeb();
|
||||
qweb.addTemplate("sometemplate", '<div>hello <t t-esc="name"/></div>');
|
||||
To avoid element rendering, a placeholder element `<t>` is also available, which executes its directive but doesn’t generate any output in and of itself.
|
||||
|
||||
// result is a vnode which represent <div>hello world</div>
|
||||
result = qweb.render("sometemplate", { name: "world" });
|
||||
```xml
|
||||
<div>
|
||||
<span t-if="somecondition">Some string</span>
|
||||
<ul t-else="1">
|
||||
<li t-foreach="messages" t-as="message">
|
||||
<t t-esc="message">
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
## QWeb extensions
|
||||
The QWeb class in the OWL project is an implementation of that specification
|
||||
with a few interesting points:
|
||||
|
||||
- t-on directive
|
||||
- t-widget, t-props, t-key
|
||||
- t-ref
|
||||
- it compiles templates into functions that output a virtual DOM instead of a
|
||||
string. This is necessary for the component system.
|
||||
- it has a few extra directives: `t-component`, `t-on`, ...
|
||||
|
||||
## Note on white spaces
|
||||
## Directives
|
||||
|
||||
We present here a list of all standard QWeb directives:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------------ | ------------------------------------------------------------ |
|
||||
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
||||
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
|
||||
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
||||
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
|
||||
| `t-foreach`, `t-as` | [Loops](#loops) |
|
||||
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
|
||||
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
|
||||
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
||||
| `t-name` | [Defining a template (not really a directive)](#qweb-engine) |
|
||||
|
||||
The component system in Owl requires additional directives, to express various
|
||||
needs. Here is a list of all Owl specific directives:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `t-component`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
|
||||
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
|
||||
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
|
||||
| `t-on-*` | [Event handling](component.md#event-handling) |
|
||||
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
||||
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
|
||||
| `t-slot` | [Rendering a slot](component.md#slots) |
|
||||
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
|
||||
|
||||
## QWeb Engine
|
||||
|
||||
This section is about the javascript code that implements the `QWeb` specification.
|
||||
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
|
||||
instantiated:
|
||||
|
||||
```js
|
||||
const qweb = new owl.QWeb();
|
||||
```
|
||||
|
||||
It's API is quite simple:
|
||||
|
||||
- **`constructor(data)`**: constructor. Takes an optional string to add initial
|
||||
templates (see `addTemplates` for more information on format of the string).
|
||||
|
||||
```js
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
```
|
||||
|
||||
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
|
||||
|
||||
```js
|
||||
qweb.addTemplate("mytemplate", "<div>hello</div>");
|
||||
```
|
||||
|
||||
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply return whenever a template is added for a second time. Otherwise, `QWeb` will crash.
|
||||
|
||||
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
|
||||
attribute).
|
||||
|
||||
```js
|
||||
const TEMPLATES = `
|
||||
<templates>
|
||||
<div t-name="App" class="main">main</div>
|
||||
<div t-name="OtherComponent">other component</div>
|
||||
</templates>`;
|
||||
qweb.addTemplates(TEMPLATES);
|
||||
```
|
||||
|
||||
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
|
||||
which is a virtual representation of the DOM (see [vdom doc](vdom.md)).
|
||||
|
||||
```js
|
||||
const vnode = qweb.render("App", component);
|
||||
```
|
||||
|
||||
- **`register(name, Component)`**: static function to register an OWL Component
|
||||
to QWeb's global registry. Globally registered Components can be used in
|
||||
templates (see the `t-component` directive). This is useful for commonly used
|
||||
components accross the application.
|
||||
|
||||
```js
|
||||
class Dialog extends owl.Component { ... }
|
||||
QWeb.register("Dialog", Dialog);
|
||||
|
||||
...
|
||||
|
||||
class ParentComponent extends owl.Component { ... }
|
||||
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
|
||||
```
|
||||
|
||||
In some way, a `QWeb` instance is the core of an Owl application. It is the only
|
||||
mandatory element of an [environment](component.md#environment). As such, it
|
||||
has an extra responsability: it can act as an event bus for internal communication
|
||||
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
|
||||
|
||||
## Reference
|
||||
|
||||
We define in this section the specification of how `QWeb` templates should be
|
||||
rendered. Note that we only document here the standard QWeb specification. Owl
|
||||
specific extensions are documented in various other parts of the documentation.
|
||||
|
||||
### White Spaces
|
||||
|
||||
White spaces in a templates are handled in a special way:
|
||||
|
||||
- consecutive whitespaces are always condensed to a single whitespace
|
||||
- if a whitespace-only text node contains a linebreak, it is ignored
|
||||
- the previous rules do not apply if we are in a `<pre>` tag
|
||||
|
||||
### Root Nodes
|
||||
|
||||
For many reasons, Owl QWeb templates should have a single root node. More
|
||||
precisely, the result of a template rendering should have a single root node:
|
||||
|
||||
```xml
|
||||
<!–– not ok: two root nodes ––>
|
||||
<t>
|
||||
<div>foo</div>
|
||||
<div>bar</div>
|
||||
</t>
|
||||
|
||||
<!–– ok: result has one single root node ––>
|
||||
<t>
|
||||
<div t-if="someCondition">foo</div>
|
||||
<span t-else="1">bar</span>
|
||||
</t>
|
||||
```
|
||||
|
||||
Extra root nodes will actually be ignored (even though they will be rendered
|
||||
in memory).
|
||||
|
||||
Note: this does not apply to subtemplates (see the `t-call` directive). In that
|
||||
case, they will be inlined in the main template, and can actually have many
|
||||
root nodes.
|
||||
|
||||
### Expression Evaluation
|
||||
|
||||
QWeb expressions are strings that will be processed at compile time. Each variable in
|
||||
the javascript expression will be replaced by a lookup in the context (so, the
|
||||
component). For example, `a + b.c(d)` will be converted into:
|
||||
|
||||
```js
|
||||
context["a"] + context["b"].c(context["d"]);
|
||||
```
|
||||
|
||||
It is useful to explain the various rules that applies on these expressions:
|
||||
|
||||
1. it should be a simple expression which returns a value. It cannot be a statement.
|
||||
|
||||
```xml
|
||||
<div><p t-if="1 + 2 === 3">ok</p></div>
|
||||
```
|
||||
|
||||
is valid, but the following is not valid:
|
||||
|
||||
```xml
|
||||
<div><p t-if="console.log(1)">NOT valid</p></div>
|
||||
```
|
||||
|
||||
2. it can use anything in the rendering context (typically, the component):
|
||||
|
||||
```xml
|
||||
<p t-if="user.birthday === today()">Happy bithday!</p>
|
||||
```
|
||||
|
||||
is valid, and will read the `user` object from the context, and call the
|
||||
`today` function.
|
||||
|
||||
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
|
||||
`&` or `|`. This is useful to make sure that we still write valid XML.
|
||||
|
||||
| Word | will be replaced by |
|
||||
| ----- | ------------------- |
|
||||
| `and` | `&&` |
|
||||
| `or` | `\|\|` |
|
||||
| `gt` | `>` |
|
||||
| `gte` | `>=` |
|
||||
| `lt` | `<` |
|
||||
| `lte` | `<=` |
|
||||
|
||||
So, one can write this:
|
||||
|
||||
```xml
|
||||
<div><p t-if="10 + 2 gt 5">ok</p></div>
|
||||
```
|
||||
|
||||
### Static Html Nodes
|
||||
|
||||
Normal, regular html nodes are rendered into themselves:
|
||||
|
||||
```xml
|
||||
<div>hello</div> <!–– rendered as itself ––>
|
||||
```
|
||||
|
||||
### Outputting Data
|
||||
|
||||
The `t-esc` directive is necessary whenever you want to add a dynamic text
|
||||
expression in a template. The text is escaped to avoid security issues.
|
||||
|
||||
```xml
|
||||
<p><t t-esc="value"/></p>
|
||||
```
|
||||
|
||||
rendered with the value `value` set to `42` in the rendering context yields:
|
||||
|
||||
```html
|
||||
<p>42</p>
|
||||
```
|
||||
|
||||
The `t-raw` directive is almost the same as `t-esc`, but without the escaping.
|
||||
This is mostly useful to inject a raw html string somewhere. Obviously, this
|
||||
is unsafe to do in general, and should only be used for strings known to be safe.
|
||||
|
||||
```xml
|
||||
<p><t t-raw="value"/></p>
|
||||
```
|
||||
|
||||
rendered with the value `value` set to `<span>foo</span>` in the rendering context yields:
|
||||
|
||||
```html
|
||||
<p><span>foo</span></p>
|
||||
```
|
||||
|
||||
### Setting Variables
|
||||
|
||||
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
|
||||
|
||||
This is done via the `t-set` directive, which takes the name of the variable to create. The value to set can be provided in two ways:
|
||||
|
||||
1. a `t-value` attribute containing an expression, and the result of its
|
||||
evaluation will be set:
|
||||
|
||||
```xml
|
||||
<t t-set="foo" t-value="2 + 1"/>
|
||||
<t t-esc="foo"/>
|
||||
```
|
||||
|
||||
will print `3`. Note that the evaluation is done at rendering time, not at
|
||||
compilte time.
|
||||
|
||||
2. if there is no `t-value` attribute, the node’s body is saved and its value is
|
||||
set as the variable’s value:
|
||||
|
||||
```xml
|
||||
<t t-set="foo">
|
||||
<li>ok</li>
|
||||
</t>
|
||||
<t t-esc="foo"/>
|
||||
```
|
||||
|
||||
will generate `<li>ok</li>` (the content is escaped as we used the `t-esc` directive)
|
||||
|
||||
The `t-set` directive acts like a regular variable in most programming language.
|
||||
It is lexically scoped (inner nodes are sub scopes), can be shadowed, ...
|
||||
|
||||
### Conditionals
|
||||
|
||||
The `t-if` directive is useful to conditionally render something. It evaluates
|
||||
the expression given as attribute value, and then acts accordingly.
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<t t-if="condition">
|
||||
<p>ok</p>
|
||||
</t>
|
||||
</div>
|
||||
```
|
||||
|
||||
The element is rendered if the condition (evaluated with the current rendering
|
||||
context) is true:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<p>ok</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
but if the condition is false it is removed from the result:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
</div>
|
||||
```
|
||||
|
||||
The conditional rendering applies to the bearer of the directive, which does not
|
||||
have to be `<t>`:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<p t-if="condition">ok</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
will give the same results as the previous example.
|
||||
|
||||
Extra conditional branching directives `t-elif` and `t-else` are also available:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<p t-if="user.birthday == today()">Happy bithday!</p>
|
||||
<p t-elif="user.login == 'root'">Welcome master!</p>
|
||||
<p t-else="">Welcome!</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Dynamic Attributes
|
||||
|
||||
One can use the `t-att-` directive to add dynamic attributes. Its main use is to
|
||||
evaluate an expression (at rendering time) and bind an attribute to its result:
|
||||
|
||||
For example, if we have `id` set to 32 in the rendering context,
|
||||
|
||||
```xml
|
||||
<div t-att-data-action-id="id"/> <!-- result: <div data-action-id="32"></div> -->
|
||||
```
|
||||
|
||||
If an expression evaluates to a falsy value, it will not be set at all:
|
||||
|
||||
```xml
|
||||
<div t-att-foo="false"/> <!-- result: <div></div> -->
|
||||
```
|
||||
|
||||
There is another way to format a string attribute: the `t-attf-` directive. With
|
||||
it, you get string interpolation:
|
||||
|
||||
```xml
|
||||
<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>
|
||||
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
|
||||
```
|
||||
|
||||
### Loops
|
||||
|
||||
QWeb has an iteration directive `t-foreach` which take an expression returning the
|
||||
collection to iterate on, and a second parameter `t-as` providing the name to use
|
||||
for the current item of the iteration:
|
||||
|
||||
```xml
|
||||
<t t-foreach="[1, 2, 3]" t-as="i">
|
||||
<p><t t-esc="i"/></p>
|
||||
</t>
|
||||
```
|
||||
|
||||
will be rendered as:
|
||||
|
||||
```xml
|
||||
<p>1</p>
|
||||
<p>2</p>
|
||||
<p>3</p>
|
||||
```
|
||||
|
||||
Like conditions, `t-foreach` applies to the element bearing the directive’s attribute, and
|
||||
|
||||
```xml
|
||||
<p t-foreach="[1, 2, 3]" t-as="i">
|
||||
<t t-esc="i"/>
|
||||
</p>
|
||||
```
|
||||
|
||||
is equivalent to the previous example.
|
||||
|
||||
`t-foreach` can iterate on an array (the current item will be the current value)
|
||||
or an object (the current item will be the current key).
|
||||
|
||||
In addition to the name passed via t-as, `t-foreach` provides a few other
|
||||
variables for various data points (note: `$as` will be replaced by the name
|
||||
passed to `t-as`):
|
||||
|
||||
- `$as_value`: the current iteration value, identical to `$as` for lists and
|
||||
integers, but for objects, it provides the value (where `$as` provides the key)
|
||||
- `$as_index`: the current iteration index (the first item of the iteration has index 0)
|
||||
- `$as_first`: whether the current item is the first of the iteration
|
||||
(equivalent to `$as_index == 0`)
|
||||
- `$as_last`: whether the current item is the last of the iteration
|
||||
(equivalent to `$as_index + 1 == $as_size`), requires the iteratee’s size be
|
||||
available
|
||||
|
||||
These extra variables provided and all new variables created into the `t-foreach`
|
||||
are only available in the scope of the `t-foreach`. If the variable exists outside
|
||||
the context of the `t-foreach`, the value is copied at the end of the foreach
|
||||
into the global context.
|
||||
|
||||
```xml
|
||||
<t t-set="existing_variable" t-value="False"/>
|
||||
<!-- existing_variable now False -->
|
||||
|
||||
<p t-foreach="Array(3)" t-as="i">
|
||||
<t t-set="existing_variable" t-value="True"/>
|
||||
<t t-set="new_variable" t-value="True"/>
|
||||
<!-- existing_variable and new_variable now True -->
|
||||
</p>
|
||||
|
||||
<!-- existing_variable always True -->
|
||||
<!-- new_variable undefined -->
|
||||
```
|
||||
|
||||
### Rendering Sub Templates
|
||||
|
||||
QWeb templates can be used for top level rendering, but they can also be used
|
||||
from within another template (to avoid duplication or give names to parts of
|
||||
templates), using the `t-call` directive:
|
||||
|
||||
```xml
|
||||
<div t-name="other-template">
|
||||
<p><t t-value="var"/></p>
|
||||
</div>
|
||||
|
||||
<div t-name="main-template">
|
||||
<t t-set="var" t-value="owl"/>
|
||||
<t t-call="other-template"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
will be rendered as `<div><p>owl</p></div>`. This example shows that the sub
|
||||
template is rendered with the execution context of the parent. The sub template
|
||||
is actually inlined in the main template, but in a sub scope: variables defined
|
||||
in the sub template do not escape.
|
||||
|
||||
Sometimes, one might want to pass information to the sub template. In that case,
|
||||
the content of the body of the `t-call` directive is available as a special
|
||||
magic variable `0`:
|
||||
|
||||
```xml
|
||||
<t t-name="other-template">
|
||||
This template was called with content:
|
||||
<t t-raw="0"/>
|
||||
</t>
|
||||
|
||||
<div t-name="main-template">
|
||||
<t t-call="other-template">
|
||||
<em>content</em>
|
||||
</t>
|
||||
</div>
|
||||
```
|
||||
|
||||
will result in :
|
||||
|
||||
```xml
|
||||
<div>
|
||||
This template was called with content:
|
||||
<em>content</em>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
The javascript QWeb implementation provides two useful debugging directives:
|
||||
|
||||
`t-debug` adds a debugger statement during template rendering:
|
||||
|
||||
```xml
|
||||
<t t-if="a_test">
|
||||
<t t-debug="">
|
||||
</t>
|
||||
```
|
||||
|
||||
will stop execution if the browser dev tools are open.
|
||||
|
||||
`t-log` takes an expression parameter, evaluates the expression during rendering and logs its result with console.log:
|
||||
|
||||
```xml
|
||||
<t t-set="foo" t-value="42"/>
|
||||
<t t-log="foo"/>
|
||||
```
|
||||
|
||||
will print 42 to the console
|
||||
|
||||
+11
-4
@@ -1,14 +1,21 @@
|
||||
# Odoo Web Library Documentation
|
||||
|
||||
# 🦉 OWL Documentation 🦉
|
||||
|
||||
## Reference
|
||||
|
||||
- [Animations](animations.md)
|
||||
- [Component](component.md)
|
||||
- [Event Bus](event_bus.md)
|
||||
- [Observer](observer.md)
|
||||
- [QWeb](qweb.md)
|
||||
- [Store](store.md)
|
||||
- [Observer](observer.md)
|
||||
- [Utils](utils.md)
|
||||
- [Virtual DOM](vdom.md)
|
||||
|
||||
## Miscellaneous
|
||||
## Learning Resources
|
||||
|
||||
- [Quick Start](quick_start.md)
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
- [Comparison with React/Vue](comparison.md)
|
||||
- [Tooling](tooling.md)
|
||||
|
||||
+256
-6
@@ -1,8 +1,258 @@
|
||||
# OWL: State Management
|
||||
# 🦉 Store 🦉
|
||||
|
||||
Managing the state in an application is not an easy task. Many different
|
||||
architectures/designs/systems/... have been created. We propose here to use
|
||||
the idea of a central store.
|
||||
## Content
|
||||
|
||||
- Store
|
||||
- connect
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
- [Public API](#public-api)
|
||||
- [Mutations](#mutations)
|
||||
- [Actions](#actions)
|
||||
- [Getters](#getters)
|
||||
- [Connecting a Component](#connecting-a-component)
|
||||
- [Semantics](#semantics)
|
||||
- [Good Practices](#good-practices)
|
||||
|
||||
## Overview
|
||||
|
||||
Managing the state in an application is not an easy task. In some cases, the
|
||||
state of an application can be part of the component tree, in a natural way.
|
||||
However, there are situations where some part of the state need to be displayed
|
||||
in various parts of the user interface, and then, it is not obvious which
|
||||
component should own which part of the state.
|
||||
|
||||
Owl's solution to this issue is a centralized store. It is a class that owns
|
||||
some state, and let the developer update it in a structured way (through
|
||||
mutations and actions). Owl components can then connect to the store, and will
|
||||
be updated if necessary.
|
||||
|
||||
Note: Owl's store is inspired by React Redux and VueX.
|
||||
|
||||
## Example
|
||||
|
||||
Here is what a simple store looks like:
|
||||
|
||||
```js
|
||||
const actions = {
|
||||
addTodo({ commit }, message) {
|
||||
commit("addTodo", message);
|
||||
}
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
addTodo({ state }, message) {
|
||||
const todo = {
|
||||
id: state.nextId++,
|
||||
message,
|
||||
isCompleted: false
|
||||
};
|
||||
state.todos.push(todo);
|
||||
}
|
||||
};
|
||||
|
||||
const state = {
|
||||
todos: [],
|
||||
nextId: 1
|
||||
};
|
||||
|
||||
const store = new owl.Store({ state, actions, mutations });
|
||||
store.on("update", () => console.log(store.state));
|
||||
|
||||
// updating the state
|
||||
store.dispatch("addTodo", "fix all bugs");
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
The store is a simple [`owl.EventBus`](event_bus.md) that triggers `update` events whenever its
|
||||
state is changed. Note that these events are triggered only after a microtask
|
||||
tick, so only one event will be triggered for any number of state changes in a
|
||||
call stack.
|
||||
|
||||
Also, it is important to mention that the state is observed (with an `owl.Observer`),
|
||||
which is the reason why it is able to know if it was changed. This implies that
|
||||
state changes need to be done carefully in some cases (adding a new key to an
|
||||
object, or modifying an array with the `arr[i] = newValue` syntax). See the
|
||||
[Observer](observer.md)'s documentation for more details.
|
||||
|
||||
### Public API
|
||||
|
||||
1. `constructor`
|
||||
2. `commit`
|
||||
3. `dispatch`
|
||||
|
||||
### Mutations
|
||||
|
||||
Mutations are the only way to modify the state. Changing the state outside a
|
||||
mutation is not allowed (and should throw an error). Mutations are synchronous.
|
||||
|
||||
```js
|
||||
const mutations = {
|
||||
setLoginState({ state }, loginState) {
|
||||
state.loginState = loginState;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Mutations are called with the `commit` method on the store, and can receive an arbitrary number of arguments.
|
||||
|
||||
```js
|
||||
store.commit("setLoginState", "error");
|
||||
```
|
||||
|
||||
### Actions
|
||||
|
||||
Actions are used to coordinate state changes. It is also useful whenever some
|
||||
asynchronous logic is necessary. For example, fetching data should be done
|
||||
in an action.
|
||||
|
||||
```js
|
||||
const actions = {
|
||||
async login({ commit }, info) {
|
||||
commit("setLoginState", "pending");
|
||||
try {
|
||||
const loginInfo = await doSomeRPC("/login/", info);
|
||||
commit("setLoginState", loginInfo);
|
||||
} catch {
|
||||
commit("setLoginState", "error");
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Actions are called with the `dispatch` method on the store, and can receive an
|
||||
arbitrary number of arguments.
|
||||
|
||||
```js
|
||||
store.dispatch("login", someInfo);
|
||||
```
|
||||
|
||||
### Getters
|
||||
|
||||
Usually, data contained in the store will be stored in a normalized way. For
|
||||
example,
|
||||
|
||||
```js
|
||||
{
|
||||
posts: [{id: 11, authorId: 4, content: 'Greetings'}],
|
||||
authors: [{id: 4, name: 'John'}]
|
||||
}
|
||||
```
|
||||
|
||||
However, the user interface will probably need some denormalized data like
|
||||
|
||||
```js
|
||||
{id: 11, author: {id: 4, name: 'John'}, content: 'Greetings'}
|
||||
```
|
||||
|
||||
This is what `getters` are for: they give a centralized way to process and
|
||||
transform the data contained in the store.
|
||||
|
||||
```js
|
||||
const getters = {
|
||||
getPost({ state }, id) {
|
||||
const post = state.posts.find(p => p.id === id);
|
||||
const author = state.authors.find(a => (a.id = post.id));
|
||||
return {
|
||||
id,
|
||||
author,
|
||||
content: post.content
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// somewhere else
|
||||
const post = store.getters.getPost(id);
|
||||
```
|
||||
|
||||
Getters take *at most* one argument.
|
||||
|
||||
|
||||
Note that getters are cached if they don't take any argument, or their argument
|
||||
is a string or a number.
|
||||
|
||||
### Connecting a Component
|
||||
|
||||
By default, an Owl `Component` is not connected to any store. The `connect`
|
||||
function is there to create sub Components that are connected versions of
|
||||
Components.
|
||||
|
||||
```javascript
|
||||
const actions = {
|
||||
increment({ commit }) {
|
||||
commit("increment", 1);
|
||||
}
|
||||
};
|
||||
const mutations = {
|
||||
increment({ state }, val) {
|
||||
state.counter += val;
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
counter: 0
|
||||
};
|
||||
const store = new owl.Store({ state, actions, mutations });
|
||||
|
||||
class Counter extends owl.Component {
|
||||
increment() {
|
||||
this.env.store.dispatch("increment");
|
||||
}
|
||||
}
|
||||
function mapStoreToProps(state) {
|
||||
return {
|
||||
value: state.counter
|
||||
};
|
||||
}
|
||||
const ConnectedCounter = owl.connect(Counter, mapStoreToProps);
|
||||
|
||||
const counter = new ConnectedCounter({ store, qweb });
|
||||
```
|
||||
|
||||
```xml
|
||||
<button t-name="Counter" t-on-click="increment">
|
||||
Click Me! [<t t-esc="props.value"/>]
|
||||
</button>
|
||||
```
|
||||
|
||||
The arguments of `connect` are:
|
||||
|
||||
- `Counter`: an owl `Component` to connect
|
||||
- `mapStoreToProps`: a function that extracts the `props` of the Component
|
||||
from the `state` of the `Store` and returns them as a dict
|
||||
- `options`: dictionary of optional parameters that may contain
|
||||
- `getStore`: a function that takes the `env` in arguments and returns an
|
||||
instance of `Store` to connect to (if not given, connects to `env.store`)
|
||||
- `hashFunction`: the function to use to detect changes in the state (if not
|
||||
given, generates a function that uses revision numbers, incremented at
|
||||
each state change)
|
||||
- `deep`: [only useful if no hashFunction is given] if false, only watch
|
||||
for top level state changes (true by default)
|
||||
|
||||
The `connect` function returns a sub class of the given `Component` which is
|
||||
connected to the `store`.
|
||||
|
||||
### Semantics
|
||||
|
||||
The `Store` and the `connect` function try to be smart and to optimize as much
|
||||
as possible the rendering and update process. What is important to know is:
|
||||
|
||||
- components are always updated in the order of their creation (so, parent
|
||||
before children)
|
||||
- they are updated only if they are in the DOM
|
||||
- if a parent is asynchronous, the system will wait for it to complete its
|
||||
update before updating other components.
|
||||
- in general, updates are not coordinated. This is not a problem for synchronous
|
||||
components, but if there are many asynchronous components, this could lead to
|
||||
a situation where some part of the UI is updated and other parts of the UI is
|
||||
not updated.
|
||||
|
||||
### Good Practices
|
||||
|
||||
- avoid asynchronous components as much as possible. Asynchronous components
|
||||
lead to situations where parts of the UI is not updated immediately.
|
||||
- do not be afraid to connect many components, parent or children if needed. For
|
||||
example, a `MessageList` component could get a list of ids in its `mapStoreToProps` and a `Message` component could get the data of its own
|
||||
message
|
||||
- since the `mapStoreToProps` function is called for each connected component,
|
||||
for each state update, it is important to make sure that these functions are
|
||||
as fast as possible.
|
||||
@@ -0,0 +1,55 @@
|
||||
# 🦉 Tooling 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Development Mode](#development-mode)
|
||||
- [Playground](#playground)
|
||||
- [Benchmarks](#benchmarks)
|
||||
|
||||
## Overview
|
||||
|
||||
To help work with/improve/learn OWL, there are a few extras tools/settings.
|
||||
|
||||
- development mode: enable better error reporting for the developer
|
||||
- a playground application: a space to experiment and learn Owl.
|
||||
- a benchmarks application: allow comparison with a few common frameworks
|
||||
|
||||
The two applications are available in the `tools/` folder, and can be accessed
|
||||
by using a static http server. A simple python
|
||||
server is available in `server.py`. There is also a npm script to start it:
|
||||
`npm run tools` (and its version with a watcher: `npm run tools:watch`).
|
||||
|
||||
## Development Mode
|
||||
|
||||
By default, Owl is in _production_ mode, this means that it will try to do its
|
||||
job fast, and skip some expensive operations. However, in some cases, it is
|
||||
convenient to have better information on what is going on, this is the purpose
|
||||
of the dev mode.
|
||||
|
||||
Owl has a mode flag, in `owl.__info__.mode`. Its default value is `prod`, but
|
||||
it can be set to `dev`:
|
||||
|
||||
```js
|
||||
owl.__info__.mode = "dev";
|
||||
```
|
||||
|
||||
Note that templates compiled with the `prod` settings will not be recompiled.
|
||||
So, changing this setting is best done at startup.
|
||||
|
||||
## Playground
|
||||
|
||||
The playground is an important application designed to help learning and
|
||||
experimenting with Owl. The last published version of Owl can be tested [online](https://odoo.github.io/owl/playground/).
|
||||
|
||||
It is an application similar to `jsFiddle`, but specialized for Owl: there are
|
||||
three tabs (`js`, `css` and `xml`), and a simple button `Run` to execute that
|
||||
code in an iframe.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Note: This is more an internal tool, useful for people working on Owl.
|
||||
|
||||
The benchmarks application is a very small application, implemented in different
|
||||
frameworks, and in different versions of Owl. This is a simple internal tool,
|
||||
useful to compare various performance metrics on some tasks.
|
||||
@@ -0,0 +1,65 @@
|
||||
# 🦉 Utils 🦉
|
||||
|
||||
Owl export a few useful utility functions, to help with common issues. Those
|
||||
functions are all available in the `owl.utils` namespace.
|
||||
|
||||
## Content
|
||||
|
||||
- [`whenReady`](#whenready): executing code when DOM is ready
|
||||
- [`loadJS`](#loadjs): loading script files
|
||||
- [`loadTemplates`](#loadtemplates): loading xml files
|
||||
- [`escape`](#escape): sanitizing strings
|
||||
- [`debounce`](#debounce): limiting rate of function calls
|
||||
|
||||
## `whenReady`
|
||||
|
||||
The function `whenReady` returns a `Promise` resolved when the DOM is ready (if
|
||||
not ready yet, resolved directly otherwise). If called with a callback as
|
||||
argument, it executes it as soon as the DOM ready (or directly).
|
||||
|
||||
```js
|
||||
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([
|
||||
templates
|
||||
]) {
|
||||
const qweb = new owl.QWeb(templates);
|
||||
const app = new App({ qweb });
|
||||
app.mount(document.body);
|
||||
});
|
||||
```
|
||||
|
||||
```js
|
||||
owl.utils.whenReady(function() {
|
||||
const qweb = new owl.QWeb();
|
||||
const app = new App({ qweb });
|
||||
app.mount(document.body);
|
||||
});
|
||||
```
|
||||
|
||||
## `loadJS`
|
||||
|
||||
`loadJS` takes a url (string) for a javascript resource, and loads it. It returns
|
||||
a promise, so the caller can properly react when it is ready. Also, it is smart:
|
||||
it maintains a list of urls previously loaded (or currently being loaded), and
|
||||
prevent doing twice the work.
|
||||
|
||||
```js
|
||||
class MyComponent extends owl.Component {
|
||||
willStart() {
|
||||
return owl.utils.loadJS("/static/libs/someLib.js");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `loadTemplates`
|
||||
|
||||
```js
|
||||
async function makeEnv() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const qweb = new owl.QWeb(templates);
|
||||
return { qweb };
|
||||
}
|
||||
```
|
||||
|
||||
## `escape`
|
||||
|
||||
## `debounce`
|
||||
+17
-2
@@ -1,3 +1,18 @@
|
||||
# VDom
|
||||
# 🦉 VDom 🦉
|
||||
|
||||
todo
|
||||
Owl is a declarative component system: we declare the structure of the component
|
||||
tree, and Owl will translate that to a list of imperative operations. This
|
||||
translation is done by a virtual dom. This is the low level layer of Owl, most
|
||||
developer will not need to call directly the virtual dom functions.
|
||||
|
||||
The main idea behind a virtual dom is to keep a in-memory representation of the
|
||||
DOM (called a virtual node), and whenever some change is needed, to regenerate
|
||||
a new representation, compute the difference between the old and the new, then
|
||||
apply the changes.
|
||||
|
||||
`vdom` exports two functions:
|
||||
|
||||
- `h`: create a new virtual node
|
||||
- `patch`: compare two virtual nodes, and apply the difference.
|
||||
|
||||
Note: Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
|
||||
|
||||
+13
-12
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "owl",
|
||||
"version": "0.8.0",
|
||||
"version": "0.16.0",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"build:js": "tsc --target esnext --module es6 --outDir dist/owl src/*",
|
||||
"build:js:es5": "tsc --target es5 --module es6 --outDir dist/owl src/*",
|
||||
"build:js": "tsc --target esnext --module es6 --outDir dist/owl",
|
||||
"build:bundle": "rollup -c",
|
||||
"build": "npm run build:js && npm run build:bundle",
|
||||
"build:es5": "npm run build:js:es5 && npm run build:bundle",
|
||||
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
|
||||
"dev": "npm-run-all --parallel \"build:* -- --watch\"",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
"test:watch": "jest --watch",
|
||||
"tools:serve": "python3 tools/server.py || python tools/server.py",
|
||||
"tools": "npm run build && npm run tools:serve",
|
||||
"pretools:watch": "npm run build",
|
||||
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -34,16 +35,13 @@
|
||||
"rollup": "^1.6.0",
|
||||
"rollup-plugin-typescript2": "^0.20.1",
|
||||
"sass": "^1.16.1",
|
||||
"snabbdom-to-html": "^5.1.1",
|
||||
"source-map-support": "^0.5.10",
|
||||
"ts-jest": "^23.10.5",
|
||||
"typescript": "^3.2.2",
|
||||
"uglify-es": "^3.3.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"jest-environment-jsdom": "^24.7.1",
|
||||
"snabbdom": "^0.7.3"
|
||||
"uglify-es": "^3.3.9",
|
||||
"jest-environment-jsdom": "^24.7.1"
|
||||
},
|
||||
"dependencies": {},
|
||||
"jest": {
|
||||
"roots": [
|
||||
"<rootDir>/src",
|
||||
@@ -62,5 +60,8 @@
|
||||
"json",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 100
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,6 +9,6 @@ export default {
|
||||
format: "iife",
|
||||
name: "owl",
|
||||
extend: true,
|
||||
outro: `exports._version = '${version}';\nexports._date = '${new Date().toISOString()}';\nexports._hash = '${git.short()}';\nexports._url = 'https://github.com/odoo/owl';`
|
||||
outro: `exports.__info__.version = '${version}';\nexports.__info__.date = '${new Date().toISOString()}';\nexports.__info__.hash = '${git.short()}';\nexports.__info__.url = 'https://github.com/odoo/owl';`
|
||||
}
|
||||
};
|
||||
|
||||
+430
-189
@@ -1,29 +1,49 @@
|
||||
import { EventBus } from "./event_bus";
|
||||
import { Observer } from "./observer";
|
||||
import { QWeb } from "./qweb";
|
||||
import { idGenerator } from "./utils";
|
||||
import { QWeb, CompiledTemplate } from "./qweb_core";
|
||||
import { h, patch, VNode } from "./vdom";
|
||||
|
||||
let getId = idGenerator();
|
||||
/**
|
||||
* Owl Component System
|
||||
*
|
||||
* This file introduces a declarative and composable component system. It
|
||||
* contains:
|
||||
*
|
||||
* - the Env interface (generic type for the environment)
|
||||
* - the Meta interface (the owl specific metadata attached to a component)
|
||||
* - the Component class
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types/helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* An Env (environment) is an object that will be (mostly) shared between all
|
||||
* components of an Owl application. It is the location which should contain
|
||||
* the qweb instance necessary to render all components.
|
||||
*
|
||||
* Note that it is totally fine to extend the environment with application
|
||||
* specific keys/objects/whatever. For example, a key `isMobile` (to declare
|
||||
* if we are in "mobile" mode), or a shared bus could be useful.
|
||||
*/
|
||||
export interface Env {
|
||||
qweb: QWeb;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is mostly an internal detail of implementation. The Meta interface is
|
||||
* useful to typecheck and describe the internal keys used by Owl to manage the
|
||||
* component tree.
|
||||
*/
|
||||
export interface Meta<T extends Env, Props> {
|
||||
readonly id: number;
|
||||
vnode: VNode | null;
|
||||
isStarted: boolean;
|
||||
isMounted: boolean;
|
||||
isDestroyed: boolean;
|
||||
parent: Component<T, any, any> | null;
|
||||
children: { [key: number]: Component<T, any, any> };
|
||||
// children mapping: from templateID to widgetID
|
||||
// should it be a map number => Widget?
|
||||
// children mapping: from templateID to componentID
|
||||
// should it be a map number => Component?
|
||||
cmap: { [key: number]: number };
|
||||
|
||||
renderId: number;
|
||||
@@ -31,25 +51,30 @@ export interface Meta<T extends Env, Props> {
|
||||
renderPromise: Promise<VNode> | null;
|
||||
boundHandlers: { [key: number]: any };
|
||||
observer?: Observer;
|
||||
render?: CompiledTemplate;
|
||||
mountedHandlers: { [key: number]: Function };
|
||||
classObj?: { [key: string]: boolean };
|
||||
}
|
||||
|
||||
export interface Type<T> extends Function {
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
// If a component does not define explicitely a template
|
||||
// key, it needs to find a template with its name (or a parent's). This is
|
||||
// qweb dependant, so we need a place to store this information indexed by
|
||||
// qweb instances.
|
||||
const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Widget
|
||||
// Component
|
||||
//------------------------------------------------------------------------------
|
||||
let nextId = 1;
|
||||
|
||||
export class Component<
|
||||
T extends Env,
|
||||
Props extends {},
|
||||
State extends {}
|
||||
> extends EventBus {
|
||||
export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
readonly __owl__: Meta<Env, Props>;
|
||||
template: string = "default";
|
||||
inlineTemplate: string | null = null;
|
||||
template?: string;
|
||||
|
||||
/**
|
||||
* The `el` is the root element of the component. Note that it could be null:
|
||||
* this is the case if the component is not mounted yet, or is destroyed.
|
||||
*/
|
||||
get el(): HTMLElement | null {
|
||||
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
|
||||
}
|
||||
@@ -57,6 +82,11 @@ export class Component<
|
||||
env: T;
|
||||
state?: State;
|
||||
props: Props;
|
||||
|
||||
// type of props is not easily representable in typescript...
|
||||
static props?: any;
|
||||
static defaultProps?: any;
|
||||
|
||||
refs: {
|
||||
[key: string]: Component<T, any, any> | HTMLElement | undefined;
|
||||
} = {};
|
||||
@@ -68,31 +98,36 @@ export class Component<
|
||||
/**
|
||||
* Creates an instance of Component.
|
||||
*
|
||||
* The root widget of a component tree needs an environment:
|
||||
* The root component of a component tree needs an environment:
|
||||
*
|
||||
* ```javascript
|
||||
* const root = new RootWidget(env, props);
|
||||
* const root = new RootComponent(env, props);
|
||||
* ```
|
||||
*
|
||||
* Every other widget simply needs a reference to its parent:
|
||||
* Every other component simply needs a reference to its parent:
|
||||
*
|
||||
* ```javascript
|
||||
* const child = new SomeWidget(parent, props);
|
||||
* const child = new SomeComponent(parent, props);
|
||||
* ```
|
||||
*
|
||||
* Note that most of the time, only the root widget needs to be created by
|
||||
* hand. Other widgets should be created automatically by the framework (with
|
||||
* the t-widget directive in a template)
|
||||
* Note that most of the time, only the root component needs to be created by
|
||||
* hand. Other components should be created automatically by the framework (with
|
||||
* the t-component directive in a template)
|
||||
*/
|
||||
constructor(parent: Component<T, any, any> | T, props?: Props) {
|
||||
super();
|
||||
|
||||
const defaultProps = (<any>this.constructor).defaultProps;
|
||||
if (defaultProps) {
|
||||
props = this.__applyDefaultProps(props, defaultProps);
|
||||
}
|
||||
if (QWeb.dev) {
|
||||
this.__validateProps(props || {});
|
||||
}
|
||||
// is this a good idea?
|
||||
// Pro: if props is empty, we can create easily a widget
|
||||
// Pro: if props is empty, we can create easily a component
|
||||
// Con: this is not really safe
|
||||
// Pro: but creating widget (by a template) is always unsafe anyway
|
||||
// Pro: but creating component (by a template) is always unsafe anyway
|
||||
this.props = <Props>props || <Props>{};
|
||||
let id: number = getId();
|
||||
let id: number = nextId++;
|
||||
let p: Component<T, any, any> | null = null;
|
||||
if (parent instanceof Component) {
|
||||
p = parent;
|
||||
@@ -100,11 +135,23 @@ export class Component<
|
||||
parent.__owl__.children[id] = this;
|
||||
} else {
|
||||
this.env = parent;
|
||||
this.env.qweb.on("update", this, () => {
|
||||
if (this.__owl__.isMounted) {
|
||||
this.render(true);
|
||||
}
|
||||
if (this.__owl__.isDestroyed) {
|
||||
// this is unlikely to happen, but if a root widget is destroyed,
|
||||
// we want to remove our subscription. The usual way to do that
|
||||
// would be to perform some check in the destroy method, but since
|
||||
// it is very performance sensitive, and since this is a rare event,
|
||||
// we simply do it lazily
|
||||
this.env.qweb.off("update", this);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.__owl__ = {
|
||||
id: id,
|
||||
vnode: null,
|
||||
isStarted: false,
|
||||
isMounted: false,
|
||||
isDestroyed: false,
|
||||
parent: p,
|
||||
@@ -113,7 +160,8 @@ export class Component<
|
||||
renderId: 1,
|
||||
renderPromise: null,
|
||||
renderProps: props || null,
|
||||
boundHandlers: {}
|
||||
boundHandlers: {},
|
||||
mountedHandlers: {}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -123,7 +171,7 @@ export class Component<
|
||||
*
|
||||
* It will be called exactly once before the initial rendering. It is useful
|
||||
* in some cases, for example, to load external assets (such as a JS library)
|
||||
* before the widget is rendered.
|
||||
* before the component is rendered.
|
||||
*
|
||||
* Note that a slow willStart method will slow down the rendering of the user
|
||||
* interface. Therefore, some effort should be made to make this method as
|
||||
@@ -160,8 +208,10 @@ export class Component<
|
||||
* It is not called on the initial render. This is useful to get some
|
||||
* information which are in the DOM. For example, the current position of the
|
||||
* scrollbar
|
||||
*
|
||||
* The return value of willPatch will be given to the patched function.
|
||||
*/
|
||||
willPatch() {}
|
||||
willPatch(): any {}
|
||||
|
||||
/**
|
||||
* This hook is called whenever a component did actually update its props,
|
||||
@@ -171,12 +221,14 @@ export class Component<
|
||||
* with the DOM (for example, through an external library) whenever the
|
||||
* component was updated.
|
||||
*
|
||||
* Updating the widget state in this hook is possible, but not encouraged.
|
||||
* Updating the component state in this hook is possible, but not encouraged.
|
||||
* One need to be careful, because updates here will cause rerender, which in
|
||||
* turn will cause other calls to updated. So, we need to be particularly
|
||||
* careful at avoiding endless cycles.
|
||||
*
|
||||
* The snapshot parameter is the result of the call to willPatch.
|
||||
*/
|
||||
patched() {}
|
||||
patched(snapshot: any) {}
|
||||
|
||||
/**
|
||||
* willUnmount is a hook that is called each time just before a component is
|
||||
@@ -194,102 +246,84 @@ export class Component<
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Attach a child widget to a given html element
|
||||
* Mount the component to a target element.
|
||||
*
|
||||
* This is most of the time not necessary, since widgets should primarily be
|
||||
* created/managed with the t-widget directive in a qweb template. However,
|
||||
* for the cases where we need more control, this method will do what is
|
||||
* necessary to make sure all the proper hooks are called (for example,
|
||||
* mounted/willUnmount)
|
||||
*
|
||||
* Note that this method makes a few assumptions:
|
||||
* - the child widget is indeed a child of the current widget
|
||||
* - the target is inside the dom of the current widget (typically a ref)
|
||||
* This should only be done if the component was created manually. Components
|
||||
* created declaratively in templates are managed by the Owl system.
|
||||
*/
|
||||
attachChild(child: Component<T, any, any>, target: HTMLElement) {
|
||||
target.appendChild(child.el!);
|
||||
child.__mount();
|
||||
}
|
||||
|
||||
async mount(target: HTMLElement): Promise<void> {
|
||||
const vnode = await this._prepare();
|
||||
const vnode = await this.__prepare();
|
||||
if (this.__owl__.isDestroyed) {
|
||||
// widget was destroyed before we get here...
|
||||
// component was destroyed before we get here...
|
||||
return;
|
||||
}
|
||||
this._patch(vnode);
|
||||
this.__patch(vnode);
|
||||
target.appendChild(this.el!);
|
||||
|
||||
if (document.body.contains(target)) {
|
||||
this._visitSubTree(w => {
|
||||
if (!w.__owl__.isMounted && this.el!.contains(w.el)) {
|
||||
w.__owl__.isMounted = true;
|
||||
w.mounted();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
this.__callMounted();
|
||||
}
|
||||
}
|
||||
|
||||
unmount() {
|
||||
if (this.el) {
|
||||
this._visitSubTree(w => {
|
||||
if (w.__owl__.isMounted) {
|
||||
w.willUnmount();
|
||||
w.__owl__.isMounted = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
this.el.remove();
|
||||
if (this.__owl__.isMounted) {
|
||||
this.__callWillUnmount();
|
||||
this.el!.remove();
|
||||
}
|
||||
}
|
||||
|
||||
async render(force: boolean = false): Promise<void> {
|
||||
if (this.__owl__.isDestroyed) {
|
||||
async render(force: boolean = false, patchQueue?: any[]): Promise<void> {
|
||||
const __owl__ = this.__owl__;
|
||||
if (!__owl__.isMounted) {
|
||||
return;
|
||||
}
|
||||
const renderVDom = this._render(force);
|
||||
const renderId = this.__owl__.renderId;
|
||||
const vnode = await renderVDom;
|
||||
if (renderId === this.__owl__.renderId) {
|
||||
const shouldPatch: boolean = !patchQueue;
|
||||
if (shouldPatch) {
|
||||
patchQueue = [];
|
||||
}
|
||||
const renderVDom = this.__render(force, patchQueue);
|
||||
const renderId = __owl__.renderId;
|
||||
await renderVDom;
|
||||
|
||||
if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) {
|
||||
// we only update the vnode and the actual DOM if no other rendering
|
||||
// occurred between now and when the render method was initially called.
|
||||
this._patch(vnode);
|
||||
this.__applyPatchQueue(<any[]>patchQueue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the component. This operation is quite complex:
|
||||
* - it recursively destroy all children
|
||||
* - call the willUnmount hooks if necessary
|
||||
* - remove the dom node from the dom
|
||||
*
|
||||
* This should only be called manually if you created the component. Most
|
||||
* components will be automatically destroyed.
|
||||
*/
|
||||
destroy() {
|
||||
if (!this.__owl__.isDestroyed) {
|
||||
for (let id in this.__owl__.children) {
|
||||
this.__owl__.children[id].destroy();
|
||||
const __owl__ = this.__owl__;
|
||||
if (!__owl__.isDestroyed) {
|
||||
const el = this.el;
|
||||
this.__destroy(__owl__.parent);
|
||||
if (el) {
|
||||
el.remove();
|
||||
}
|
||||
if (this.__owl__.isMounted) {
|
||||
this.willUnmount();
|
||||
}
|
||||
if (this.el) {
|
||||
this.el.remove();
|
||||
this.__owl__.isMounted = false;
|
||||
delete this.__owl__.vnode;
|
||||
}
|
||||
if (this.__owl__.parent) {
|
||||
let id = this.__owl__.id;
|
||||
delete this.__owl__.parent.__owl__.children[id];
|
||||
this.__owl__.parent = null;
|
||||
}
|
||||
this.clear();
|
||||
this.__owl__.isDestroyed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by the component system whenever its props are
|
||||
* updated. If it returns true, then the component will be rendered.
|
||||
* Otherwise, it will skip the rendering (also, its props will not be updated)
|
||||
*/
|
||||
shouldUpdate(nextProps: Props): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is the correct way to update the environment of a widget. Doing
|
||||
* this will cause a full rerender of the widget and its children, so this is
|
||||
* This method is the correct way to update the environment of a component. Doing
|
||||
* this will cause a full rerender of the component and its children, so this is
|
||||
* an operation that should not be done frequently.
|
||||
*
|
||||
* A good usecase for updating the environment would be to update some mostly
|
||||
@@ -297,135 +331,342 @@ export class Component<
|
||||
* mode or not.
|
||||
*/
|
||||
async updateEnv(nextEnv: Partial<T>): Promise<void> {
|
||||
if (this.__owl__.parent && this.__owl__.parent.env === this.env) {
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.parent && __owl__.parent.env === this.env) {
|
||||
this.env = Object.create(this.env);
|
||||
}
|
||||
Object.assign(this.env, nextEnv);
|
||||
if (this.__owl__.isMounted) {
|
||||
if (__owl__.isMounted) {
|
||||
await this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
async updateProps(
|
||||
nextProps: Props,
|
||||
forceUpdate: boolean = false
|
||||
): Promise<void> {
|
||||
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
|
||||
return shouldUpdate ? this._updateProps(nextProps) : Promise.resolve();
|
||||
}
|
||||
|
||||
set(target: any, key: string | number, value: any) {
|
||||
this.__owl__.observer!.set(target, key, value);
|
||||
/**
|
||||
* Emit a custom event of type 'eventType' with the given 'payload' on the
|
||||
* component's el, if it exists. However, note that the event will only bubble
|
||||
* up to the parent DOM nodes. Thus, it must be called between mounted() and
|
||||
* willUnmount().
|
||||
*/
|
||||
trigger(eventType: string, payload?: any) {
|
||||
if (this.el) {
|
||||
const ev = new CustomEvent(eventType, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail: payload
|
||||
});
|
||||
this.el.dispatchEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
async _updateProps(nextProps: Props): Promise<void> {
|
||||
await this.willUpdateProps(nextProps);
|
||||
this.props = nextProps;
|
||||
await this.render();
|
||||
__destroy(parent: Component<any, any, any> | null) {
|
||||
const __owl__ = this.__owl__;
|
||||
const isMounted = __owl__.isMounted;
|
||||
if (isMounted) {
|
||||
this.willUnmount();
|
||||
__owl__.isMounted = false;
|
||||
}
|
||||
const children = __owl__.children;
|
||||
for (let key in children) {
|
||||
children[key].__destroy(this);
|
||||
}
|
||||
if (parent) {
|
||||
let id = __owl__.id;
|
||||
delete parent.__owl__.children[id];
|
||||
__owl__.parent = null;
|
||||
}
|
||||
__owl__.isDestroyed = true;
|
||||
delete __owl__.vnode;
|
||||
}
|
||||
|
||||
_patch(vnode) {
|
||||
this.__owl__.renderPromise = null;
|
||||
if (this.__owl__.vnode) {
|
||||
this.willPatch();
|
||||
this.__owl__.vnode = patch(this.__owl__.vnode, vnode);
|
||||
this.patched();
|
||||
} else {
|
||||
this.__owl__.vnode = patch(document.createElement(vnode.sel!), vnode);
|
||||
__callMounted() {
|
||||
const __owl__ = this.__owl__;
|
||||
const children = __owl__.children;
|
||||
for (let id in children) {
|
||||
const comp = children[id];
|
||||
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
|
||||
comp.__callMounted();
|
||||
}
|
||||
}
|
||||
__owl__.isMounted = true;
|
||||
const handlers = __owl__.mountedHandlers;
|
||||
for (let key in handlers) {
|
||||
handlers[key]();
|
||||
}
|
||||
this.mounted();
|
||||
}
|
||||
|
||||
__callWillUnmount() {
|
||||
this.willUnmount();
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.isMounted = false;
|
||||
const children = __owl__.children;
|
||||
for (let id in children) {
|
||||
const comp = children[id];
|
||||
if (comp.__owl__.isMounted) {
|
||||
comp.__callWillUnmount();
|
||||
}
|
||||
}
|
||||
}
|
||||
async _prepare(): Promise<VNode> {
|
||||
this.__owl__.renderProps = this.props;
|
||||
this.__owl__.renderPromise = this.willStart().then(() => {
|
||||
if (this.__owl__.isDestroyed) {
|
||||
return Promise.resolve(h("div"));
|
||||
|
||||
async __updateProps(
|
||||
nextProps: Props,
|
||||
forceUpdate: boolean = false,
|
||||
patchQueue?: any[]
|
||||
): Promise<void> {
|
||||
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
|
||||
if (shouldUpdate) {
|
||||
const defaultProps = (<any>this.constructor).defaultProps;
|
||||
if (defaultProps) {
|
||||
nextProps = this.__applyDefaultProps(nextProps, defaultProps);
|
||||
}
|
||||
this.__owl__.isStarted = true;
|
||||
if (this.inlineTemplate) {
|
||||
this.env.qweb.addTemplate(
|
||||
this.inlineTemplate,
|
||||
this.inlineTemplate,
|
||||
true
|
||||
);
|
||||
if (QWeb.dev) {
|
||||
this.__validateProps(nextProps);
|
||||
}
|
||||
this._observeState();
|
||||
return this._render();
|
||||
});
|
||||
return this.__owl__.renderPromise;
|
||||
await this.willUpdateProps(nextProps);
|
||||
this.props = nextProps;
|
||||
await this.render(forceUpdate, patchQueue);
|
||||
}
|
||||
}
|
||||
|
||||
async _render(force: boolean = false): Promise<VNode> {
|
||||
this.__owl__.renderId++;
|
||||
__patch(vnode) {
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.renderPromise = null;
|
||||
const target = __owl__.vnode || document.createElement(vnode.sel!);
|
||||
if (this.__owl__.classObj) {
|
||||
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, this.__owl__.classObj);
|
||||
}
|
||||
__owl__.vnode = patch(target, vnode);
|
||||
}
|
||||
|
||||
__prepare(): Promise<VNode> {
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.renderProps = this.props;
|
||||
__owl__.renderPromise = this.__prepareAndRender();
|
||||
return __owl__.renderPromise;
|
||||
}
|
||||
|
||||
async __prepareAndRender(): Promise<VNode> {
|
||||
await this.willStart();
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.isDestroyed) {
|
||||
return Promise.resolve(h("div"));
|
||||
}
|
||||
const qweb = this.env.qweb;
|
||||
if (!this.template) {
|
||||
let tmap = TEMPLATE_MAP[qweb.id];
|
||||
if (!tmap) {
|
||||
tmap = {};
|
||||
TEMPLATE_MAP[qweb.id] = tmap;
|
||||
}
|
||||
let p = (<any>this).constructor;
|
||||
let name: string = p.name;
|
||||
let template = tmap[name];
|
||||
if (template) {
|
||||
this.template = template;
|
||||
} else {
|
||||
while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
|
||||
p = p.__proto__;
|
||||
}
|
||||
if (p === Component) {
|
||||
throw new Error(`Could not find template for component "${this.constructor.name}"`);
|
||||
} else {
|
||||
tmap[name] = template;
|
||||
this.template = template;
|
||||
}
|
||||
}
|
||||
}
|
||||
__owl__.render = qweb.render.bind(qweb, this.template);
|
||||
this.__observeState();
|
||||
return this.__render();
|
||||
}
|
||||
|
||||
async __render(force: boolean = false, patchQueue: any[] = []): Promise<VNode> {
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.renderId++;
|
||||
const promises: Promise<void>[] = [];
|
||||
const template = this.inlineTemplate || this.template;
|
||||
if (this.__owl__.observer) {
|
||||
this.__owl__.observer.allowMutations = false;
|
||||
const patch: any[] = [this];
|
||||
if (__owl__.isMounted) {
|
||||
patchQueue.push(patch);
|
||||
}
|
||||
let vnode = this.env.qweb.render(template, this, {
|
||||
if (__owl__.observer) {
|
||||
__owl__.observer.allowMutations = false;
|
||||
}
|
||||
let vnode = __owl__.render!(this, {
|
||||
promises,
|
||||
handlers: this.__owl__.boundHandlers,
|
||||
forceUpdate: force
|
||||
handlers: __owl__.boundHandlers,
|
||||
mountedHandlers: __owl__.mountedHandlers,
|
||||
forceUpdate: force,
|
||||
patchQueue
|
||||
});
|
||||
if (this.__owl__.observer) {
|
||||
this.__owl__.observer.allowMutations = true;
|
||||
patch.push(vnode);
|
||||
if (__owl__.observer) {
|
||||
__owl__.observer.allowMutations = true;
|
||||
}
|
||||
|
||||
// this part is critical for the patching process to be done correctly. The
|
||||
// tricky part is that a child widget can be rerendered on its own, which
|
||||
// tricky part is that a child component can be rerendered on its own, which
|
||||
// will update its own vnode representation without the knowledge of the
|
||||
// parent widget. With this, we make sure that the parent widget will be
|
||||
// parent component. With this, we make sure that the parent component will be
|
||||
// able to patch itself properly after
|
||||
vnode.key = this.__owl__.id;
|
||||
this.__owl__.renderProps = this.props;
|
||||
this.__owl__.renderPromise = Promise.all(promises).then(() => vnode);
|
||||
return this.__owl__.renderPromise;
|
||||
vnode.key = __owl__.id;
|
||||
__owl__.renderProps = this.props;
|
||||
__owl__.renderPromise = Promise.all(promises).then(() => vnode);
|
||||
return __owl__.renderPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only called by qweb t-widget directive
|
||||
* Only called by qweb t-component directive
|
||||
*/
|
||||
_mount(vnode: VNode, elm: HTMLElement): VNode {
|
||||
this.__owl__.vnode = patch(elm, vnode);
|
||||
this.__mount();
|
||||
return this.__owl__.vnode;
|
||||
__mount(vnode: VNode, elm: HTMLElement): VNode {
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.classObj) {
|
||||
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, __owl__.classObj);
|
||||
}
|
||||
__owl__.vnode = patch(elm, vnode);
|
||||
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
|
||||
this.__callMounted();
|
||||
}
|
||||
return __owl__.vnode;
|
||||
}
|
||||
|
||||
__mount() {
|
||||
if (this.__owl__.isMounted) {
|
||||
return;
|
||||
/**
|
||||
* Only called by qweb t-component directive (when t-keepalive is set)
|
||||
*/
|
||||
__remount() {
|
||||
const __owl__ = this.__owl__;
|
||||
if (!__owl__.isMounted) {
|
||||
__owl__.isMounted = true;
|
||||
this.mounted();
|
||||
}
|
||||
this._observeState();
|
||||
if (this.__owl__.parent) {
|
||||
if (this.__owl__.parent!.__owl__.isMounted) {
|
||||
this.__owl__.isMounted = true;
|
||||
this.mounted();
|
||||
const children = this.__owl__.children;
|
||||
for (let id in children) {
|
||||
children[id].__mount();
|
||||
}
|
||||
|
||||
__observeState() {
|
||||
if (this.state) {
|
||||
const __owl__ = this.__owl__;
|
||||
__owl__.observer = new Observer();
|
||||
__owl__.observer.observe(this.state);
|
||||
__owl__.observer.notifyCB = this.render.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply default props (only top level).
|
||||
*
|
||||
* Note that this method does not modify in place the props, it returns a new
|
||||
* prop object
|
||||
*/
|
||||
__applyDefaultProps(props: Object | undefined, defaultProps: Object): Props {
|
||||
props = props ? Object.create(props) : {};
|
||||
for (let propName in defaultProps) {
|
||||
if (props![propName] === undefined) {
|
||||
props![propName] = defaultProps[propName];
|
||||
}
|
||||
}
|
||||
return <Props>props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given patch queue. A patch is a pair [c, vn], where c is a
|
||||
* Component instance and vn a VNode.
|
||||
* 1) Call 'willPatch' on the component of each patch
|
||||
* 2) Call '__patch' on the component of each patch
|
||||
* 3) Call 'patched' on the component of each patch, in inverse order
|
||||
*/
|
||||
__applyPatchQueue(patchQueue: any[]) {
|
||||
const patchLen = patchQueue.length;
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
const patch = patchQueue[i];
|
||||
patch.push(patch[0].willPatch());
|
||||
}
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
const patch = patchQueue[i];
|
||||
patch[0].__patch(patch[1]);
|
||||
}
|
||||
for (let i = patchLen - 1; i >= 0; i--) {
|
||||
const patch = patchQueue[i];
|
||||
patch[0].patched(patch[2]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the component props (or next props) against the (static) props
|
||||
* description. This is potentially an expensive operation: it may needs to
|
||||
* visit recursively the props and all the children to check if they are valid.
|
||||
* This is why it is only done in 'dev' mode.
|
||||
*/
|
||||
__validateProps(props: Object) {
|
||||
const propsDef = (<any>this.constructor).props;
|
||||
if (propsDef instanceof Array) {
|
||||
// list of strings (prop names)
|
||||
for (let i = 0, l = propsDef.length; i < l; i++) {
|
||||
if (!(propsDef[i] in props)) {
|
||||
throw new Error(`Missing props '${propsDef[i]}' (component '${this.constructor.name}')`);
|
||||
}
|
||||
}
|
||||
} else if (propsDef) {
|
||||
// propsDef is an object now
|
||||
for (let propName in propsDef) {
|
||||
if (!(propName in props)) {
|
||||
if (propsDef[propName] && !propsDef[propName].optional) {
|
||||
throw new Error(`Missing props '${propName}' (component '${this.constructor.name}')`);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let isValid = isValidProp(props[propName], propsDef[propName]);
|
||||
if (!isValid) {
|
||||
throw new Error(
|
||||
`Props '${propName}' of invalid type in component '${this.constructor.name}'`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_visitSubTree(callback: (w: Component<T, any, any>) => boolean) {
|
||||
const shouldVisitChildren = callback(this);
|
||||
if (shouldVisitChildren) {
|
||||
const children = this.__owl__.children;
|
||||
for (let id in children) {
|
||||
children[id]._visitSubTree(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_observeState() {
|
||||
if (this.state) {
|
||||
this.__owl__.observer = new Observer();
|
||||
this.__owl__.observer.observe(this.state);
|
||||
this.__owl__.observer.notifyCB = this.render.bind(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Prop validation helper
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check if an invidual prop value matches its (static) prop definition
|
||||
*/
|
||||
function isValidProp(prop, propDef): boolean {
|
||||
if (typeof propDef === "function") {
|
||||
// Check if a value is constructed by some Constructor. Note that there is a
|
||||
// slight abuse of language: we want to consider primitive values as well.
|
||||
//
|
||||
// So, even though 1 is not an instance of Number, we want to consider that
|
||||
// it is valid.
|
||||
if (typeof prop === "object") {
|
||||
return prop instanceof propDef;
|
||||
}
|
||||
return typeof prop === propDef.name.toLowerCase();
|
||||
} else if (propDef instanceof Array) {
|
||||
// If this code is executed, this means that we want to check if a prop
|
||||
// matches at least one of its descriptor.
|
||||
let result = false;
|
||||
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
|
||||
result = result || isValidProp(prop, propDef[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// propsDef is an object
|
||||
let result = isValidProp(prop, propDef.type);
|
||||
if (propDef.type === Array) {
|
||||
for (let i = 0, iLen = prop.length; i < iLen; i++) {
|
||||
result = result && isValidProp(prop[i], propDef.element);
|
||||
}
|
||||
}
|
||||
if (propDef.type === Object) {
|
||||
const shape = propDef.shape;
|
||||
for (let key in shape) {
|
||||
result = result && isValidProp(prop[key], shape[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+5
-3
@@ -3,7 +3,8 @@
|
||||
* - emit events
|
||||
* - add/remove listeners.
|
||||
*
|
||||
* This is a useful pattern of communication in some cases.
|
||||
* This is a useful pattern of communication in many cases. For OWL, each
|
||||
* components and stores are event buses.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -28,7 +29,7 @@ export class EventBus {
|
||||
* Add a listener for the 'eventType' events.
|
||||
*
|
||||
* Note that the 'owner' of this event can be anything, but will more likely
|
||||
* be a widget or a class. The idea is that the callback will be called with
|
||||
* be a component or a class. The idea is that the callback will be called with
|
||||
* the proper owner bound.
|
||||
*
|
||||
* Also, the owner should be kind of unique. This will be used to remove the
|
||||
@@ -63,7 +64,8 @@ export class EventBus {
|
||||
*/
|
||||
trigger(eventType: string, ...args: any[]) {
|
||||
const subs = this.subscriptions[eventType] || [];
|
||||
for (let sub of subs) {
|
||||
for (let i = 0, iLen = subs.length; i < iLen; i++) {
|
||||
const sub = subs[i];
|
||||
sub.callback.call(sub.owner, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
+33
-1
@@ -1,7 +1,39 @@
|
||||
/**
|
||||
* This file is the main file packaged by rollup (see rollup.config.js). From
|
||||
* this file, we export all public owl elements.
|
||||
*
|
||||
* Note that dynamic values, such as a date or a commit hash are added by rollup
|
||||
*/
|
||||
export { Component } from "./component";
|
||||
export { EventBus } from "./event_bus";
|
||||
export { QWeb } from "./qweb";
|
||||
export { Observer } from "./observer";
|
||||
|
||||
// we need to import manually the extra directives so they can register
|
||||
// themselves in QWeb, otherwise these files will not even be loaded.
|
||||
import "./qweb_directives";
|
||||
import "./qweb_extensions";
|
||||
import { QWeb } from "./qweb_core";
|
||||
export { QWeb };
|
||||
|
||||
export { connect, Store } from "./store";
|
||||
import * as _utils from "./utils";
|
||||
|
||||
export const __info__ = {};
|
||||
|
||||
Object.defineProperty(__info__, "mode", {
|
||||
get() {
|
||||
return QWeb.dev ? "dev" : "prod";
|
||||
},
|
||||
set(mode: string) {
|
||||
QWeb.dev = mode === "dev";
|
||||
if (QWeb.dev) {
|
||||
const url = `https://github.com/odoo/owl/blob/master/doc/tooling.md#development-mode`;
|
||||
console.warn(
|
||||
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
|
||||
);
|
||||
} else {
|
||||
console.log(`Owl is now running in 'prod' mode.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
export const utils = _utils;
|
||||
|
||||
+83
-31
@@ -1,26 +1,40 @@
|
||||
/**
|
||||
* Owl Observer
|
||||
*
|
||||
* This code contains the logic that allows Owl to observe and react to state
|
||||
* changes.
|
||||
*
|
||||
* This is a Observer class that can observe any JS values. The way it works
|
||||
* can be summarized thusly:
|
||||
* - primitive values are not observed at all
|
||||
* - Objects are observed by replacing all their keys with getters/setters
|
||||
* (recursively)
|
||||
* - Arrays are observed by replacing their prototype with a customized version,
|
||||
* which wrap some methods to allow the tracking of each state change.
|
||||
*
|
||||
* Note that this code is inspired by Vue.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Observer
|
||||
// Modified Array prototype
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const methodsToPatch = [
|
||||
"push",
|
||||
"pop",
|
||||
"shift",
|
||||
"unshift",
|
||||
"splice",
|
||||
"sort",
|
||||
"reverse"
|
||||
];
|
||||
// we define here a new modified Array prototype, which basically override all
|
||||
// Array methods that change some state to be able to track their changes
|
||||
const methodsToPatch = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];
|
||||
const methodLen = methodsToPatch.length;
|
||||
|
||||
const ArrayProto = Array.prototype;
|
||||
const ModifiedArrayProto = Object.create(ArrayProto);
|
||||
|
||||
for (let method of methodsToPatch) {
|
||||
for (let i = 0; i < methodLen; i++) {
|
||||
const method = methodsToPatch[i];
|
||||
const initialMethod = ArrayProto[method];
|
||||
ModifiedArrayProto[method] = function(...args) {
|
||||
if (!this.__observer__.allowMutations) {
|
||||
throw new Error(`Array cannot be changed here")`);
|
||||
}
|
||||
this.__observer__.rev++;
|
||||
this.__observer__.notifyChange();
|
||||
this.__owl__.rev++;
|
||||
let parent = this;
|
||||
@@ -38,22 +52,38 @@ for (let method of methodsToPatch) {
|
||||
break;
|
||||
}
|
||||
if (inserted) {
|
||||
for (let elem of inserted) {
|
||||
this.__observer__.observe(elem, this);
|
||||
for (let i = 0, iLen = inserted.length; i < iLen; i++) {
|
||||
this.__observer__.observe(inserted[i], this);
|
||||
}
|
||||
}
|
||||
return initialMethod.call(this, ...args);
|
||||
};
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Observer
|
||||
//------------------------------------------------------------------------------
|
||||
export class Observer {
|
||||
rev: number = 1;
|
||||
allowMutations: boolean = true;
|
||||
dirty: boolean = false;
|
||||
|
||||
static set(target: any, key: number | string, value: any) {
|
||||
if (!target.__owl__) {
|
||||
throw Error("`Observer.set()` can only be called with observed Objects or Arrays");
|
||||
}
|
||||
target.__owl__.observer.set(target, key, value);
|
||||
}
|
||||
|
||||
static delete(target: any, key: number | string) {
|
||||
if (!target.__owl__) {
|
||||
throw Error("`Observer.delete()` can only be called with observed Objects");
|
||||
}
|
||||
target.__owl__.observer.delete(target, key);
|
||||
}
|
||||
|
||||
notifyCB() {}
|
||||
notifyChange() {
|
||||
this.rev++;
|
||||
this.dirty = true;
|
||||
Promise.resolve().then(() => {
|
||||
if (this.dirty) {
|
||||
@@ -84,37 +114,55 @@ export class Observer {
|
||||
}
|
||||
|
||||
set(target: any, key: number | string, value: any) {
|
||||
this._addProp(target, key, value);
|
||||
target.__owl__.rev++;
|
||||
let alreadyDefined =
|
||||
key in target && Object.getOwnPropertyDescriptor(target, key)!.configurable === false;
|
||||
if (alreadyDefined) {
|
||||
target[key] = value;
|
||||
} else {
|
||||
this._addProp(target, key, value);
|
||||
this._updateRevNumber(target);
|
||||
}
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
delete(target: any, key: number | string) {
|
||||
delete target[key];
|
||||
this._updateRevNumber(target);
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
_observeObj<T extends { __owl__?: any }>(obj: T, parent?: any) {
|
||||
const keys = Object.keys(obj);
|
||||
obj.__owl__ = { rev: 1, deepRev: 1, parent };
|
||||
obj.__owl__ = {
|
||||
rev: this.rev,
|
||||
deepRev: this.rev,
|
||||
parent,
|
||||
observer: this
|
||||
};
|
||||
Object.defineProperty(obj, "__owl__", { enumerable: false });
|
||||
for (let key of keys) {
|
||||
for (let key in obj) {
|
||||
this._addProp(obj, key, obj[key]);
|
||||
}
|
||||
}
|
||||
|
||||
_observeArr(arr: Array<any>, parent?: any) {
|
||||
(<any>arr).__owl__ = { rev: 1, deepRev: 1, parent };
|
||||
(<any>arr).__owl__ = {
|
||||
rev: this.rev,
|
||||
deepRev: this.rev,
|
||||
parent,
|
||||
observer: this
|
||||
};
|
||||
Object.defineProperty(arr, "__owl__", { enumerable: false });
|
||||
(<any>arr).__proto__ = Object.create(ModifiedArrayProto);
|
||||
(<any>arr).__proto__.__observer__ = this;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
for (let i = 0, iLen = arr.length; i < iLen; i++) {
|
||||
this.observe(arr[i], arr);
|
||||
}
|
||||
}
|
||||
|
||||
_addProp<T extends { __owl__?: any }>(
|
||||
obj: T,
|
||||
key: string | number,
|
||||
value: any
|
||||
) {
|
||||
_addProp<T extends { __owl__?: any }>(obj: T, key: string | number, value: any) {
|
||||
var self = this;
|
||||
Object.defineProperty(obj, key, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
return value;
|
||||
@@ -126,17 +174,21 @@ export class Observer {
|
||||
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
|
||||
);
|
||||
}
|
||||
self._updateRevNumber(obj);
|
||||
value = newVal;
|
||||
self.observe(newVal, obj);
|
||||
obj.__owl__.rev!++;
|
||||
let parent = obj;
|
||||
do {
|
||||
parent.__owl__.deepRev++;
|
||||
} while ((parent = parent.__owl__.parent) && parent !== obj);
|
||||
self.notifyChange();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.observe(value, obj);
|
||||
}
|
||||
_updateRevNumber(target: any) {
|
||||
this.rev++;
|
||||
target.__owl__.rev!++;
|
||||
let parent = target;
|
||||
do {
|
||||
parent.__owl__.deepRev++;
|
||||
} while ((parent = parent.__owl__.parent) && parent !== target);
|
||||
}
|
||||
}
|
||||
|
||||
-1109
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,751 @@
|
||||
import { VNode, h } from "./vdom";
|
||||
import { QWebVar, compileExpr } from "./qweb_expressions";
|
||||
import { EventBus } from "./event_bus";
|
||||
|
||||
/**
|
||||
* Owl QWeb Engine
|
||||
*
|
||||
* In this file, you will find a QWeb engine/template compiler. It is the core
|
||||
* of how Owl component works.
|
||||
*
|
||||
* Briefly, Owl QWeb compiles XML templates into functions that output a virtual
|
||||
* DOM representation.
|
||||
*
|
||||
* We have here:
|
||||
* - a CompilationContext class, which is an internal object that contains all
|
||||
* compilation specific information, while a template is being compiled.
|
||||
* - a QWeb class: this is the code of the QWeb compiler.
|
||||
*
|
||||
* Note that this file does not contain the implementation of the QWeb
|
||||
* directives (see qweb_directives.ts and qweb_extensions.ts).
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types
|
||||
//------------------------------------------------------------------------------
|
||||
export type EvalContext = { [key: string]: any };
|
||||
export type CompiledTemplate = (context: EvalContext, extra: any) => VNode;
|
||||
|
||||
interface Template {
|
||||
elem: Element;
|
||||
fn: CompiledTemplate;
|
||||
}
|
||||
|
||||
interface CompilationInfo {
|
||||
node: Element;
|
||||
qweb: QWeb;
|
||||
ctx: Context;
|
||||
fullName: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface NodeCreationCompilationInfo extends CompilationInfo {
|
||||
nodeID: number;
|
||||
addNodeHook: Function;
|
||||
}
|
||||
|
||||
export interface Directive {
|
||||
name: string;
|
||||
extraNames?: string[];
|
||||
priority: number;
|
||||
// if return true, then directive is fully applied and there is no need to
|
||||
// keep processing node. Otherwise, we keep going.
|
||||
atNodeEncounter?(info: CompilationInfo): boolean | void;
|
||||
atNodeCreation?(info: NodeCreationCompilationInfo): void;
|
||||
finalize?(info: CompilationInfo): void;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Const/global stuff/helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
|
||||
|
||||
const lineBreakRE = /[\r\n]/;
|
||||
const whitespaceRE = /\s+/g;
|
||||
|
||||
const DIRECTIVE_NAMES = {
|
||||
name: 1,
|
||||
att: 1,
|
||||
attf: 1,
|
||||
key: 1
|
||||
};
|
||||
|
||||
const DIRECTIVES: Directive[] = [];
|
||||
|
||||
const NODE_HOOKS_PARAMS = {
|
||||
create: "(_, n)",
|
||||
insert: "vn",
|
||||
remove: "(vn, rm)"
|
||||
};
|
||||
|
||||
interface Utils {
|
||||
h: typeof h;
|
||||
toObj(expr: any): Object;
|
||||
shallowEqual(p1: Object, p2: Object): boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const UTILS: Utils = {
|
||||
h: h,
|
||||
toObj(expr) {
|
||||
if (typeof expr === "string") {
|
||||
expr = expr.trim();
|
||||
if (!expr) {
|
||||
return {};
|
||||
}
|
||||
let words = expr.split(/\s+/);
|
||||
let result = {};
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
result[words[i]] = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return expr;
|
||||
},
|
||||
shallowEqual(p1, p2) {
|
||||
for (let k in p1) {
|
||||
if (p1[k] !== p2[k]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
function parseXML(xml: string): Document {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(xml, "text/xml");
|
||||
if (doc.getElementsByTagName("parsererror").length) {
|
||||
throw new Error("Invalid XML in template");
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
let nextID = 1;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// QWeb rendering engine
|
||||
//------------------------------------------------------------------------------
|
||||
export class QWeb extends EventBus {
|
||||
templates: { [name: string]: Template } = {};
|
||||
utils = UTILS;
|
||||
static components = Object.create(null);
|
||||
|
||||
// dev mode enables better error messages or more costly validations
|
||||
static dev: boolean = false;
|
||||
|
||||
// the id field is useful to be able to hash qweb instances. The current
|
||||
// use case is that component's templates are qweb dependant, and need to be
|
||||
// able to map a qweb instance to a template name.
|
||||
id = nextID++;
|
||||
|
||||
// slots contains sub templates defined with t-set inside t-component nodes, and
|
||||
// are meant to be used by the t-slot directive.
|
||||
slots = {};
|
||||
nextSlotId = 1;
|
||||
|
||||
constructor(data?: string) {
|
||||
super();
|
||||
if (data) {
|
||||
this.addTemplates(data);
|
||||
}
|
||||
}
|
||||
|
||||
static addDirective(directive: Directive) {
|
||||
DIRECTIVES.push(directive);
|
||||
DIRECTIVE_NAMES[directive.name] = 1;
|
||||
DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
|
||||
if (directive.extraNames) {
|
||||
directive.extraNames.forEach(n => (DIRECTIVE_NAMES[n] = 1));
|
||||
}
|
||||
}
|
||||
|
||||
static register(name: string, Component: any) {
|
||||
if (QWeb.components[name]) {
|
||||
throw new Error(`Component '${name}' has already been registered`);
|
||||
}
|
||||
QWeb.components[name] = Component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a template to the internal template map. Note that it is not
|
||||
* immediately compiled.
|
||||
*/
|
||||
addTemplate(name: string, xmlString: string, allowDuplicate?: boolean) {
|
||||
if (allowDuplicate && name in this.templates) {
|
||||
return;
|
||||
}
|
||||
const doc = parseXML(xmlString);
|
||||
if (!doc.firstChild) {
|
||||
throw new Error("Invalid template (should not be empty)");
|
||||
}
|
||||
this._addTemplate(name, <Element>doc.firstChild);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load templates from a xml (as a string). This will look up for the first
|
||||
* <templates> tag, and will consider each child of this as a template, with
|
||||
* the name given by the t-name attribute.
|
||||
*/
|
||||
addTemplates(xmlstr: string) {
|
||||
const doc = parseXML(xmlstr);
|
||||
const templates = doc.getElementsByTagName("templates")[0];
|
||||
if (!templates) {
|
||||
return;
|
||||
}
|
||||
for (let elem of <any>templates.children) {
|
||||
const name = elem.getAttribute("t-name");
|
||||
this._addTemplate(name, elem);
|
||||
}
|
||||
}
|
||||
|
||||
_addTemplate(name: string, elem: Element) {
|
||||
if (name in this.templates) {
|
||||
throw new Error(`Template ${name} already defined`);
|
||||
}
|
||||
this._processTemplate(elem);
|
||||
const template = {
|
||||
elem,
|
||||
fn: (context, extra) => {
|
||||
const compiledFunction = this._compile(name, elem);
|
||||
template.fn = compiledFunction;
|
||||
return compiledFunction.call(this, context, extra);
|
||||
}
|
||||
};
|
||||
this.templates[name] = template;
|
||||
}
|
||||
|
||||
_processTemplate(elem: Element) {
|
||||
let tbranch = elem.querySelectorAll("[t-elif], [t-else]");
|
||||
for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
|
||||
let node = tbranch[i];
|
||||
let prevElem = node.previousElementSibling!;
|
||||
let pattr = function(name) {
|
||||
return prevElem.getAttribute(name);
|
||||
};
|
||||
let nattr = function(name) {
|
||||
return +!!node.getAttribute(name);
|
||||
};
|
||||
if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
|
||||
if (pattr("t-foreach")) {
|
||||
throw new Error(
|
||||
"t-if cannot stay at the same level as t-foreach when using t-elif or t-else"
|
||||
);
|
||||
}
|
||||
if (
|
||||
["t-if", "t-elif", "t-else"].map(nattr).reduce(function(a, b) {
|
||||
return a + b;
|
||||
}) > 1
|
||||
) {
|
||||
throw new Error("Only one conditional branching directive is allowed per node");
|
||||
}
|
||||
// All text nodes between branch nodes are removed
|
||||
let textNode;
|
||||
while ((textNode = node.previousSibling) !== prevElem) {
|
||||
if (textNode.nodeValue.trim().length) {
|
||||
throw new Error("text is not allowed between branching directives");
|
||||
}
|
||||
textNode.remove();
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
"t-elif and t-else directives must be preceded by a t-if or t-elif directive"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Render a template
|
||||
*
|
||||
* @param {string} name the template should already have been added
|
||||
*/
|
||||
render(name: string, context: EvalContext = {}, extra: any = null): VNode {
|
||||
const template = this.templates[name];
|
||||
if (!template) {
|
||||
throw new Error(`Template ${name} does not exist`);
|
||||
}
|
||||
return template.fn.call(this, context, extra);
|
||||
}
|
||||
|
||||
_compile(name: string, elem: Element, parentNode?: number): CompiledTemplate {
|
||||
const isDebug = elem.attributes.hasOwnProperty("t-debug");
|
||||
const ctx = new Context(name);
|
||||
if (parentNode) {
|
||||
ctx.nextID = parentNode + 1;
|
||||
ctx.parentNode = parentNode;
|
||||
ctx.allowMultipleRoots = true;
|
||||
ctx.addLine(`let c${parentNode} = extra.parentNode;`);
|
||||
}
|
||||
this._compileNode(elem, ctx);
|
||||
|
||||
if (ctx.shouldProtectContext) {
|
||||
ctx.code.unshift(" context = Object.create(context);");
|
||||
}
|
||||
if (ctx.shouldDefineOwner) {
|
||||
// this is necessary to prevent some directives (t-forach for ex) to
|
||||
// pollute the rendering context by adding some keys in it.
|
||||
ctx.code.unshift(" let owner = context;");
|
||||
}
|
||||
if (ctx.shouldDefineQWeb) {
|
||||
ctx.code.unshift(" let QWeb = this.constructor;");
|
||||
}
|
||||
if (ctx.shouldDefineUtils) {
|
||||
ctx.code.unshift(" let utils = this.utils;");
|
||||
}
|
||||
|
||||
if (!parentNode) {
|
||||
if (!ctx.rootNode) {
|
||||
throw new Error("A template should have one root node");
|
||||
}
|
||||
ctx.addLine(`return vn${ctx.rootNode};`);
|
||||
}
|
||||
let template;
|
||||
try {
|
||||
template = new Function("context", "extra", ctx.code.join("\n")) as CompiledTemplate;
|
||||
} catch (e) {
|
||||
const templateName = ctx.templateName.replace(/`/g, "'");
|
||||
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
|
||||
console.warn(ctx.code.join("\n"));
|
||||
console.groupEnd();
|
||||
throw new Error(
|
||||
`Invalid generated code while compiling template '${templateName}': ${e.message}`
|
||||
);
|
||||
}
|
||||
if (isDebug) {
|
||||
const tpl = this.templates[name];
|
||||
if (tpl) {
|
||||
const msg = `Template: ${tpl.elem.outerHTML}\nCompiled code:\n${template.toString()}`;
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate code from an xml node
|
||||
*
|
||||
*/
|
||||
_compileNode(node: ChildNode, ctx: Context) {
|
||||
if (!(node instanceof Element)) {
|
||||
// this is a text node, there are no directive to apply
|
||||
let text = node.textContent!;
|
||||
if (!ctx.inPreTag) {
|
||||
if (lineBreakRE.test(text) && !text.trim()) {
|
||||
return;
|
||||
}
|
||||
text = text.replace(whitespaceRE, " ");
|
||||
}
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
|
||||
} else if (ctx.parentTextNode) {
|
||||
ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
|
||||
} else {
|
||||
// this is an unusual situation: this text node is the result of the
|
||||
// template rendering.
|
||||
let nodeID = ctx.generateID();
|
||||
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
|
||||
ctx.rootContext.rootNode = nodeID;
|
||||
ctx.rootContext.parentTextNode = nodeID;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const firstLetter = node.tagName[0];
|
||||
if (firstLetter === firstLetter.toUpperCase()) {
|
||||
// this is a component, we modify in place the xml document to change
|
||||
// <SomeComponent ... /> to <t t-component="SomeComponent" ... />
|
||||
node.setAttribute("t-component", node.tagName);
|
||||
}
|
||||
const attributes = (<Element>node).attributes;
|
||||
|
||||
const validDirectives: {
|
||||
directive: Directive;
|
||||
value: string;
|
||||
fullName: string;
|
||||
}[] = [];
|
||||
|
||||
let withHandlers = false;
|
||||
|
||||
// maybe this is not optimal: we iterate on all attributes here, and again
|
||||
// just after for each directive.
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
let attrName = attributes[i].name;
|
||||
if (attrName.startsWith("t-")) {
|
||||
let dName = attrName.slice(2).split(/-|\./)[0];
|
||||
if (!(dName in DIRECTIVE_NAMES)) {
|
||||
throw new Error(`Unknown QWeb directive: '${attrName}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DIR_N = DIRECTIVES.length;
|
||||
const ATTR_N = attributes.length;
|
||||
for (let i = 0; i < DIR_N; i++) {
|
||||
let directive = DIRECTIVES[i];
|
||||
let fullName;
|
||||
let value;
|
||||
for (let j = 0; j < ATTR_N; j++) {
|
||||
const name = attributes[j].name;
|
||||
if (
|
||||
name === "t-" + directive.name ||
|
||||
name.startsWith("t-" + directive.name + "-") ||
|
||||
name.startsWith("t-" + directive.name + ".")
|
||||
) {
|
||||
fullName = name;
|
||||
value = attributes[j].textContent;
|
||||
validDirectives.push({ directive, value, fullName });
|
||||
if (directive.name === "on" || directive.name === "model") {
|
||||
withHandlers = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let { directive, value, fullName } of validDirectives) {
|
||||
if (directive.atNodeEncounter) {
|
||||
const isDone = directive.atNodeEncounter({
|
||||
node,
|
||||
qweb: this,
|
||||
ctx,
|
||||
fullName,
|
||||
value
|
||||
});
|
||||
if (isDone) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.nodeName !== "t") {
|
||||
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
|
||||
ctx = ctx.withParent(nodeID);
|
||||
let nodeHooks = {};
|
||||
let addNodeHook = function(hook, handler) {
|
||||
nodeHooks[hook] = nodeHooks[hook] || [];
|
||||
nodeHooks[hook].push(handler);
|
||||
};
|
||||
|
||||
for (let { directive, value, fullName } of validDirectives) {
|
||||
if (directive.atNodeCreation) {
|
||||
directive.atNodeCreation({
|
||||
node,
|
||||
qweb: this,
|
||||
ctx,
|
||||
fullName,
|
||||
value,
|
||||
nodeID,
|
||||
addNodeHook
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(nodeHooks).length) {
|
||||
ctx.addLine(`p${nodeID}.hook = {`);
|
||||
for (let hook in nodeHooks) {
|
||||
ctx.addLine(` ${hook}: ${NODE_HOOKS_PARAMS[hook]} => {`);
|
||||
for (let handler of nodeHooks[hook]) {
|
||||
ctx.addLine(` ${handler}`);
|
||||
}
|
||||
ctx.addLine(` },`);
|
||||
}
|
||||
ctx.addLine(`};`);
|
||||
}
|
||||
}
|
||||
if (node.nodeName === "pre") {
|
||||
ctx = ctx.subContext("inPreTag", true);
|
||||
}
|
||||
|
||||
this._compileChildren(node, ctx);
|
||||
|
||||
for (let { directive, value, fullName } of validDirectives) {
|
||||
if (directive.finalize) {
|
||||
directive.finalize({ node, qweb: this, ctx, fullName, value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_compileGenericNode(node: ChildNode, ctx: Context, withHandlers: boolean = true): number {
|
||||
// nodeType 1 is generic tag
|
||||
if (node.nodeType !== 1) {
|
||||
throw new Error("unsupported node type");
|
||||
}
|
||||
const attributes = (<Element>node).attributes;
|
||||
const attrs: string[] = [];
|
||||
const props: string[] = [];
|
||||
const tattrs: number[] = [];
|
||||
|
||||
function handleBooleanProps(key, val) {
|
||||
let isProp = false;
|
||||
if (node.nodeName === "input" && key === "checked") {
|
||||
let type = (<Element>node).getAttribute("type");
|
||||
if (type === "checkbox" || type === "radio") {
|
||||
isProp = true;
|
||||
}
|
||||
}
|
||||
if (node.nodeName === "option" && key === "selected") {
|
||||
isProp = true;
|
||||
}
|
||||
if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
|
||||
isProp = true;
|
||||
}
|
||||
if ((key === "readonly" && node.nodeName === "input") || node.nodeName === "textarea") {
|
||||
isProp = true;
|
||||
}
|
||||
if (isProp) {
|
||||
props.push(`${key}: _${val}`);
|
||||
}
|
||||
}
|
||||
let classObj = "";
|
||||
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
let name = attributes[i].name;
|
||||
const value = attributes[i].textContent!;
|
||||
|
||||
// regular attributes
|
||||
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
|
||||
const attID = ctx.generateID();
|
||||
if (name === "class") {
|
||||
let classDef = value
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map(a => `'${a}':true`)
|
||||
.join(",");
|
||||
classObj = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${classObj} = {${classDef}};`);
|
||||
} else {
|
||||
ctx.addLine(`var _${attID} = '${value}';`);
|
||||
if (!name.match(/^[a-zA-Z]+$/)) {
|
||||
// attribute contains 'non letters' => we want to quote it
|
||||
name = '"' + name + '"';
|
||||
}
|
||||
attrs.push(`${name}: _${attID}`);
|
||||
handleBooleanProps(name, attID);
|
||||
}
|
||||
}
|
||||
|
||||
// dynamic attributes
|
||||
if (name.startsWith("t-att-")) {
|
||||
let attName = name.slice(6);
|
||||
const v = ctx.getValue(value);
|
||||
let formattedValue = v.id || ctx.formatExpression(v);
|
||||
|
||||
if (attName === "class") {
|
||||
formattedValue = `this.utils.toObj(${formattedValue})`;
|
||||
if (classObj) {
|
||||
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
|
||||
} else {
|
||||
classObj = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${classObj} = ${formattedValue};`);
|
||||
}
|
||||
} else {
|
||||
const attID = ctx.generateID();
|
||||
if (!attName.match(/^[a-zA-Z]+$/)) {
|
||||
// attribute contains 'non letters' => we want to quote it
|
||||
attName = '"' + attName + '"';
|
||||
}
|
||||
// we need to combine dynamic with non dynamic attributes:
|
||||
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
|
||||
const attValue = (<Element>node).getAttribute(attName);
|
||||
if (attValue) {
|
||||
const attValueID = ctx.generateID();
|
||||
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
|
||||
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
|
||||
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
|
||||
attrs.splice(attrIndex, 1);
|
||||
}
|
||||
ctx.addLine(`var _${attID} = ${formattedValue};`);
|
||||
attrs.push(`${attName}: _${attID}`);
|
||||
handleBooleanProps(attName, attID);
|
||||
}
|
||||
}
|
||||
|
||||
if (name.startsWith("t-attf-")) {
|
||||
let attName = name.slice(7);
|
||||
if (!attName.match(/^[a-zA-Z]+$/)) {
|
||||
// attribute contains 'non letters' => we want to quote it
|
||||
attName = '"' + attName + '"';
|
||||
}
|
||||
const formattedExpr = ctx.interpolate(value);
|
||||
const attID = ctx.generateID();
|
||||
let staticVal = (<Element>node).getAttribute(attName);
|
||||
if (staticVal) {
|
||||
ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`);
|
||||
} else {
|
||||
ctx.addLine(`var _${attID} = ${formattedExpr};`);
|
||||
}
|
||||
attrs.push(`${attName}: _${attID}`);
|
||||
}
|
||||
|
||||
// t-att= attributes
|
||||
if (name === "t-att") {
|
||||
let id = ctx.generateID();
|
||||
ctx.addLine(`var _${id} = ${ctx.formatExpression(value!)};`);
|
||||
tattrs.push(id);
|
||||
}
|
||||
}
|
||||
let nodeID = ctx.generateID();
|
||||
let nodeKey: any = (<Element>node).getAttribute("t-key");
|
||||
if (nodeKey) {
|
||||
nodeKey = ctx.formatExpression(nodeKey);
|
||||
} else {
|
||||
nodeKey = nodeID;
|
||||
}
|
||||
const parts = [`key:${nodeKey}`];
|
||||
if (attrs.length + tattrs.length > 0) {
|
||||
parts.push(`attrs:{${attrs.join(",")}}`);
|
||||
}
|
||||
if (props.length > 0) {
|
||||
parts.push(`props:{${props.join(",")}}`);
|
||||
}
|
||||
if (classObj) {
|
||||
parts.push(`class:${classObj}`);
|
||||
}
|
||||
if (withHandlers) {
|
||||
parts.push(`on:{}`);
|
||||
}
|
||||
|
||||
ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
|
||||
for (let id of tattrs) {
|
||||
ctx.addIf(`_${id} instanceof Array`);
|
||||
ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
|
||||
ctx.addElse();
|
||||
ctx.addLine(`for (let key in _${id}) {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`);
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
ctx.closeIf();
|
||||
}
|
||||
ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
|
||||
}
|
||||
|
||||
return nodeID;
|
||||
}
|
||||
|
||||
_compileChildren(node: ChildNode, ctx: Context) {
|
||||
if (node.childNodes.length > 0) {
|
||||
for (let child of Array.from(node.childNodes)) {
|
||||
this._compileNode(child, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Compilation Context
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class Context {
|
||||
nextID: number = 1;
|
||||
code: string[] = [];
|
||||
variables: { [key: string]: QWebVar } = {};
|
||||
escaping: boolean = false;
|
||||
parentNode: number | null = null;
|
||||
parentTextNode: number | null = null;
|
||||
rootNode: number | null = null;
|
||||
indentLevel: number = 0;
|
||||
rootContext: Context;
|
||||
caller: Element | undefined;
|
||||
shouldDefineOwner: boolean = false;
|
||||
shouldDefineQWeb: boolean = false;
|
||||
shouldDefineUtils: boolean = false;
|
||||
shouldProtectContext: boolean = false;
|
||||
inLoop: boolean = false;
|
||||
inPreTag: boolean = false;
|
||||
templateName: string;
|
||||
allowMultipleRoots: boolean = false;
|
||||
|
||||
constructor(name?: string) {
|
||||
this.rootContext = this;
|
||||
this.templateName = name || "noname";
|
||||
this.addLine("var h = this.utils.h;");
|
||||
}
|
||||
|
||||
generateID(): number {
|
||||
const id = this.rootContext.nextID++;
|
||||
return id;
|
||||
}
|
||||
|
||||
withParent(node: number): Context {
|
||||
if (
|
||||
!this.allowMultipleRoots &&
|
||||
this === this.rootContext &&
|
||||
(this.parentNode || this.parentTextNode)
|
||||
) {
|
||||
throw new Error("A template should not have more than one root node");
|
||||
}
|
||||
if (!this.rootContext.rootNode) {
|
||||
this.rootContext.rootNode = node;
|
||||
}
|
||||
return this.subContext("parentNode", node);
|
||||
}
|
||||
|
||||
subContext(key: keyof Context, value: any): Context {
|
||||
const newContext = Object.create(this);
|
||||
newContext[key] = value;
|
||||
return newContext;
|
||||
}
|
||||
|
||||
indent() {
|
||||
this.indentLevel++;
|
||||
}
|
||||
|
||||
dedent() {
|
||||
this.indentLevel--;
|
||||
}
|
||||
|
||||
addLine(line: string) {
|
||||
const prefix = new Array(this.indentLevel + 2).join(" ");
|
||||
this.code.push(prefix + line);
|
||||
}
|
||||
|
||||
addIf(condition: string) {
|
||||
this.addLine(`if (${condition}) {`);
|
||||
this.indent();
|
||||
}
|
||||
|
||||
addElse() {
|
||||
this.dedent();
|
||||
this.addLine("} else {");
|
||||
this.indent();
|
||||
}
|
||||
|
||||
closeIf() {
|
||||
this.dedent();
|
||||
this.addLine("}");
|
||||
}
|
||||
|
||||
getValue(val: any): any {
|
||||
return val in this.variables ? this.getValue(this.variables[val]) : val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an expression for being consumed at render time. Its main job
|
||||
* is to
|
||||
* - replace unknown variables by a lookup in the context
|
||||
* - replace already defined variables by their internal name
|
||||
*/
|
||||
formatExpression(expr: string): string {
|
||||
return compileExpr(expr, this.variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform string interpolation on the given string. Note that if the whole
|
||||
* string is an expression, it simply returns it (formatted and enclosed in
|
||||
* parentheses).
|
||||
* For instance:
|
||||
* 'Hello {{x}}!' -> `Hello ${x}`
|
||||
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
|
||||
*/
|
||||
interpolate(s: string): string {
|
||||
let matches = s.match(/\{\{.*?\}\}/g);
|
||||
if (matches && matches[0].length === s.length) {
|
||||
return `(${this.formatExpression(s.slice(2, -2))})`;
|
||||
}
|
||||
|
||||
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
|
||||
return "`" + r + "`";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
import { Context, QWeb, UTILS } from "./qweb_core";
|
||||
import { QWebExprVar } from "./qweb_expressions";
|
||||
|
||||
/**
|
||||
* Owl QWeb Directives
|
||||
*
|
||||
* This file contains the implementation of most standard QWeb directives:
|
||||
* - t-esc
|
||||
* - t-raw
|
||||
* - t-set/t-value
|
||||
* - t-if/t-elif/t-else
|
||||
* - t-call
|
||||
* - t-foreach/t-as
|
||||
* - t-debug
|
||||
* - t-log
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-esc and t-raw
|
||||
//------------------------------------------------------------------------------
|
||||
(<any>UTILS).getFragment = function(str: string): DocumentFragment {
|
||||
const temp = document.createElement("template");
|
||||
temp.innerHTML = str;
|
||||
return temp.content;
|
||||
};
|
||||
|
||||
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
||||
if (value === "0" && ctx.caller) {
|
||||
qweb._compileNode(ctx.caller, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.xml instanceof NodeList) {
|
||||
for (let node of Array.from(value.xml)) {
|
||||
qweb._compileNode(<ChildNode>node, ctx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let exprID: string;
|
||||
if (typeof value === "string") {
|
||||
exprID = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
|
||||
} else {
|
||||
exprID = value.id;
|
||||
}
|
||||
ctx.addIf(`${exprID} || ${exprID} === 0`);
|
||||
if (ctx.escaping) {
|
||||
if (ctx.parentTextNode) {
|
||||
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
|
||||
} else if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
|
||||
} else {
|
||||
let nodeID = ctx.generateID();
|
||||
ctx.rootContext.rootNode = nodeID;
|
||||
ctx.rootContext.parentTextNode = nodeID;
|
||||
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
|
||||
}
|
||||
} else {
|
||||
let fragID = ctx.generateID();
|
||||
ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`);
|
||||
let tempNodeID = ctx.generateID();
|
||||
ctx.addLine(`var p${tempNodeID} = {hook: {`);
|
||||
ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`);
|
||||
ctx.addLine(`}};`);
|
||||
ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
|
||||
ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
|
||||
}
|
||||
if (node.childNodes.length) {
|
||||
ctx.addElse();
|
||||
qweb._compileChildren(node, ctx);
|
||||
}
|
||||
|
||||
ctx.closeIf();
|
||||
}
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "esc",
|
||||
priority: 70,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
if (node.nodeName !== "t") {
|
||||
let nodeID = qweb._compileGenericNode(node, ctx);
|
||||
ctx = ctx.withParent(nodeID);
|
||||
}
|
||||
let value = ctx.getValue(node.getAttribute("t-esc")!);
|
||||
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "raw",
|
||||
priority: 80,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
if (node.nodeName !== "t") {
|
||||
let nodeID = qweb._compileGenericNode(node, ctx);
|
||||
ctx = ctx.withParent(nodeID);
|
||||
}
|
||||
let value = ctx.getValue(node.getAttribute("t-raw")!);
|
||||
compileValueNode(value, node, qweb, ctx);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-set
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "set",
|
||||
extraNames: ["value"],
|
||||
priority: 60,
|
||||
atNodeEncounter({ node, ctx }): boolean {
|
||||
const variable = node.getAttribute("t-set")!;
|
||||
let value = node.getAttribute("t-value")!;
|
||||
if (value) {
|
||||
const formattedValue = ctx.formatExpression(value);
|
||||
if (ctx.variables.hasOwnProperty(variable)) {
|
||||
ctx.addLine(`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`);
|
||||
} else {
|
||||
const varName = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`var ${varName} = ${formattedValue};`);
|
||||
ctx.variables[variable] = {
|
||||
id: varName,
|
||||
expr: formattedValue
|
||||
};
|
||||
}
|
||||
} else {
|
||||
ctx.variables[variable] = {
|
||||
xml: node.childNodes
|
||||
};
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-if, t-elif, t-else
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "if",
|
||||
priority: 20,
|
||||
atNodeEncounter({ node, ctx }): boolean {
|
||||
let cond = ctx.getValue(node.getAttribute("t-if")!);
|
||||
ctx.addIf(`${ctx.formatExpression(cond)}`);
|
||||
return false;
|
||||
},
|
||||
finalize({ ctx }) {
|
||||
ctx.closeIf();
|
||||
}
|
||||
});
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "elif",
|
||||
priority: 30,
|
||||
atNodeEncounter({ node, ctx }): boolean {
|
||||
let cond = ctx.getValue(node.getAttribute("t-elif")!);
|
||||
ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`);
|
||||
ctx.indent();
|
||||
return false;
|
||||
},
|
||||
finalize({ ctx }) {
|
||||
ctx.closeIf();
|
||||
}
|
||||
});
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "else",
|
||||
priority: 40,
|
||||
atNodeEncounter({ ctx }): boolean {
|
||||
ctx.addLine(`else {`);
|
||||
ctx.indent();
|
||||
return false;
|
||||
},
|
||||
finalize({ ctx }) {
|
||||
ctx.closeIf();
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-call
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "call",
|
||||
priority: 50,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
if (node.nodeName !== "t") {
|
||||
throw new Error("Invalid tag for t-call directive (should be 't')");
|
||||
}
|
||||
const subTemplate = node.getAttribute("t-call")!;
|
||||
const nodeTemplate = qweb.templates[subTemplate];
|
||||
if (!nodeTemplate) {
|
||||
throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
|
||||
}
|
||||
const nodeCopy = node.cloneNode(true) as Element;
|
||||
nodeCopy.removeAttribute("t-call");
|
||||
|
||||
// extract variables from nodecopy
|
||||
const tempCtx = new Context();
|
||||
tempCtx.nextID = ctx.rootContext.nextID;
|
||||
qweb._compileNode(nodeCopy, tempCtx);
|
||||
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
|
||||
ctx.rootContext.nextID = tempCtx.nextID;
|
||||
|
||||
// open new scope, if necessary
|
||||
const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
|
||||
if (hasNewVariables) {
|
||||
ctx.addLine("{");
|
||||
ctx.indent();
|
||||
// add new variables, if any
|
||||
for (let key in tempCtx.variables) {
|
||||
const v = tempCtx.variables[key];
|
||||
if ((<QWebExprVar>v).expr) {
|
||||
ctx.addLine(`let ${(<QWebExprVar>v).id} = ${(<QWebExprVar>v).expr};`);
|
||||
}
|
||||
// todo: handle XML variables...
|
||||
}
|
||||
}
|
||||
|
||||
// compile sub template
|
||||
const subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
|
||||
|
||||
qweb._compileNode(nodeTemplate.elem, subCtx);
|
||||
|
||||
// close new scope
|
||||
if (hasNewVariables) {
|
||||
ctx.dedent();
|
||||
ctx.addLine("}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-foreach
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "foreach",
|
||||
extraNames: ["as"],
|
||||
priority: 10,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
ctx.rootContext.shouldProtectContext = true;
|
||||
ctx = ctx.subContext("inLoop", true);
|
||||
const elems = node.getAttribute("t-foreach")!;
|
||||
const name = node.getAttribute("t-as")!;
|
||||
let arrayID = ctx.generateID();
|
||||
ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
|
||||
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
|
||||
let keysID = ctx.generateID();
|
||||
let valuesID = ctx.generateID();
|
||||
ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
|
||||
ctx.addIf(`!(_${arrayID} instanceof Array)`);
|
||||
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
|
||||
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
|
||||
ctx.closeIf();
|
||||
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
|
||||
ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(`context.${name}_first = i === 0;`);
|
||||
ctx.addLine(`context.${name}_last = i === _length${keysID} - 1;`);
|
||||
ctx.addLine(`context.${name}_index = i;`);
|
||||
ctx.addLine(`context.${name} = _${keysID}[i];`);
|
||||
ctx.addLine(`context.${name}_value = _${valuesID}[i];`);
|
||||
const nodeCopy = <Element>node.cloneNode(true);
|
||||
let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
|
||||
if (!shouldWarn && node.tagName === "t") {
|
||||
if (node.hasAttribute("t-component") && !node.hasAttribute("t-key")) {
|
||||
shouldWarn = true;
|
||||
}
|
||||
if (
|
||||
!shouldWarn &&
|
||||
node.children.length === 1 &&
|
||||
node.children[0].tagName !== "t" &&
|
||||
!node.children[0].hasAttribute("t-key")
|
||||
) {
|
||||
shouldWarn = true;
|
||||
}
|
||||
}
|
||||
if (shouldWarn) {
|
||||
console.warn(
|
||||
`Directive t-foreach should always be used with a t-key! (in template: '${
|
||||
ctx.templateName
|
||||
}')`
|
||||
);
|
||||
}
|
||||
nodeCopy.removeAttribute("t-foreach");
|
||||
qweb._compileNode(nodeCopy, ctx);
|
||||
ctx.dedent();
|
||||
ctx.addLine("}");
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-debug
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "debug",
|
||||
priority: 1,
|
||||
atNodeEncounter({ ctx }) {
|
||||
ctx.addLine("debugger;");
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-log
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "log",
|
||||
priority: 1,
|
||||
atNodeEncounter({ ctx, value }) {
|
||||
const expr = ctx.formatExpression(value);
|
||||
ctx.addLine(`console.log(${expr})`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* Owl QWeb Expression Parser
|
||||
*
|
||||
* Owl needs in various contexts to be able to understand the structure of a
|
||||
* string representing a javascript expression. The usual goal is to be able
|
||||
* to rewrite some variables. For example, if a template has
|
||||
*
|
||||
* ```xml
|
||||
* <t t-if="computeSomething({val: state.val})">...</t>
|
||||
* ```
|
||||
*
|
||||
* this needs to be translated in something like this:
|
||||
*
|
||||
* ```js
|
||||
* if (context["computeSomething"]({val: context["state"].val})) { ... }
|
||||
* ```
|
||||
*
|
||||
* This file contains the implementation of an extremely naive tokenizer/parser
|
||||
* and evaluator for javascript expressions. The supported grammar is basically
|
||||
* only expressive enough to understand the shape of objects, of arrays, and
|
||||
* various operators.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Misc types, constants and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(
|
||||
","
|
||||
);
|
||||
|
||||
const WORD_REPLACEMENT = {
|
||||
and: "&&",
|
||||
or: "||",
|
||||
gt: ">",
|
||||
gte: ">=",
|
||||
lt: "<",
|
||||
lte: "<="
|
||||
};
|
||||
|
||||
export interface QWebExprVar {
|
||||
id: string;
|
||||
expr: string;
|
||||
}
|
||||
|
||||
export interface QWebXMLVar {
|
||||
xml: NodeList;
|
||||
}
|
||||
|
||||
export type QWebVar = QWebExprVar | QWebXMLVar;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tokenizer
|
||||
//------------------------------------------------------------------------------
|
||||
type TKind =
|
||||
| "LEFT_BRACE"
|
||||
| "RIGHT_BRACE"
|
||||
| "LEFT_BRACKET"
|
||||
| "RIGHT_BRACKET"
|
||||
| "LEFT_PAREN"
|
||||
| "RIGHT_PAREN"
|
||||
| "COMMA"
|
||||
| "VALUE"
|
||||
| "SYMBOL"
|
||||
| "OPERATOR"
|
||||
| "COLON";
|
||||
|
||||
interface Token {
|
||||
type: TKind;
|
||||
value: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
|
||||
"{": "LEFT_BRACE",
|
||||
"}": "RIGHT_BRACE",
|
||||
"[": "LEFT_BRACKET",
|
||||
"]": "RIGHT_BRACKET",
|
||||
":": "COLON",
|
||||
",": "COMMA",
|
||||
"(": "LEFT_PAREN",
|
||||
")": "RIGHT_PAREN"
|
||||
};
|
||||
|
||||
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(",");
|
||||
|
||||
type Tokenizer = (expr: string) => Token | false;
|
||||
|
||||
let tokenizeString: Tokenizer = function(expr) {
|
||||
let s = expr[0];
|
||||
let start = s;
|
||||
if (s !== "'" && s !== '"') {
|
||||
return false;
|
||||
}
|
||||
let i = 1;
|
||||
let cur;
|
||||
while (expr[i] && expr[i] !== start) {
|
||||
cur = expr[i];
|
||||
s += cur;
|
||||
if (cur === "\\") {
|
||||
i++;
|
||||
cur = expr[i];
|
||||
if (!cur) {
|
||||
throw new Error("Invalid expression");
|
||||
}
|
||||
s += cur;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (expr[i] !== start) {
|
||||
throw new Error("Invalid expression");
|
||||
}
|
||||
s += start;
|
||||
return { type: "VALUE", value: s };
|
||||
};
|
||||
|
||||
let tokenizeNumber: Tokenizer = function(expr) {
|
||||
let s = expr[0];
|
||||
if (s && s.match(/[0-9]/)) {
|
||||
let i = 1;
|
||||
while (expr[i] && expr[i].match(/[0-9]|\./)) {
|
||||
s += expr[i];
|
||||
i++;
|
||||
}
|
||||
return { type: "VALUE", value: s };
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let tokenizeSymbol: Tokenizer = function(expr) {
|
||||
let s = expr[0];
|
||||
if (s && s.match(/[a-zA-Z_\$]/)) {
|
||||
let i = 1;
|
||||
while (expr[i] && expr[i].match(/\w/)) {
|
||||
s += expr[i];
|
||||
i++;
|
||||
}
|
||||
if (s in WORD_REPLACEMENT) {
|
||||
return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
|
||||
}
|
||||
return { type: "SYMBOL", value: s };
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const tokenizeStatic: Tokenizer = function(expr) {
|
||||
const char = expr[0];
|
||||
if (char && char in STATIC_TOKEN_MAP) {
|
||||
return { type: STATIC_TOKEN_MAP[char], value: char };
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const tokenizeOperator: Tokenizer = function(expr) {
|
||||
for (let op of OPERATORS) {
|
||||
if (expr.startsWith(op)) {
|
||||
return { type: "OPERATOR", value: op };
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const TOKENIZERS = [
|
||||
tokenizeString,
|
||||
tokenizeNumber,
|
||||
tokenizeSymbol,
|
||||
tokenizeStatic,
|
||||
tokenizeOperator
|
||||
];
|
||||
|
||||
/**
|
||||
* Convert a javascript expression (as a string) into a list of tokens. For
|
||||
* example: `tokenize("1 + b")` will return:
|
||||
* ```js
|
||||
* [
|
||||
* {type: "VALUE", value: "1"},
|
||||
* {type: "OPERATOR", value: "+"},
|
||||
* {type: "SYMBOL", value: "b"}
|
||||
* ]
|
||||
* ```
|
||||
*/
|
||||
export function tokenize(expr: string): Token[] {
|
||||
const result: Token[] = [];
|
||||
let token: boolean | Token = true;
|
||||
|
||||
while (token) {
|
||||
expr = expr.trim();
|
||||
if (expr) {
|
||||
for (let tokenizer of TOKENIZERS) {
|
||||
token = tokenizer(expr);
|
||||
if (token) {
|
||||
result.push(token);
|
||||
expr = expr.slice(token.size || token.value.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
token = false;
|
||||
}
|
||||
}
|
||||
if (expr.length) {
|
||||
throw new Error(`Tokenizer error: could not tokenize "${expr}"`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Expression "evaluator"
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This is the main function exported by this file. This is the code that will
|
||||
* process an expression (given as a string) and returns another expression with
|
||||
* proper lookups in the context.
|
||||
*
|
||||
* Usually, this kind of code would be very simple to do if we had an AST (so,
|
||||
* if we had a javascript parser), since then, we would only need to find the
|
||||
* variables and replace them. However, a parser is more complicated, and there
|
||||
* are no standard builtin parser API.
|
||||
*
|
||||
* Since this method is applied to simple javasript expressions, and the work to
|
||||
* be done is actually quite simple, we actually can get away with not using a
|
||||
* parser, which helps with the code size.
|
||||
*
|
||||
* Here is the heuristic used by this method to determine if a token is a
|
||||
* variable:
|
||||
* - by default, all symbols are considered a variable
|
||||
* - unless the previous token is a dot (in that case, this is a property: `a.b`)
|
||||
* - or if the previous token is a left brace or a comma, and the next token is
|
||||
* a colon (in that case, this is an object key: `{a: b}`)
|
||||
*/
|
||||
export function compileExpr(expr: string, vars: { [key: string]: QWebVar }): string {
|
||||
const tokens = tokenize(expr);
|
||||
let result = "";
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
let token = tokens[i];
|
||||
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
|
||||
// we need to find if it is a variable
|
||||
let isVar = true;
|
||||
let prevToken = tokens[i - 1];
|
||||
if (prevToken) {
|
||||
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
|
||||
isVar = false;
|
||||
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
|
||||
let nextToken = tokens[i + 1];
|
||||
if (nextToken && nextToken.type === "COLON") {
|
||||
isVar = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isVar) {
|
||||
if (token.value in vars && "id" in vars[token.value]) {
|
||||
token.value = (<QWebExprVar>vars[token.value]).id;
|
||||
} else {
|
||||
token.value = `context['${token.value}']`;
|
||||
}
|
||||
}
|
||||
}
|
||||
result += token.value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,727 @@
|
||||
import { QWeb, UTILS } from "./qweb_core";
|
||||
import { VNode } from "./vdom";
|
||||
|
||||
/**
|
||||
* Owl QWeb Extensions
|
||||
*
|
||||
* This file contains the implementation of non standard QWeb directives, added
|
||||
* by Owl and that will only work on Owl projects:
|
||||
*
|
||||
* - t-on
|
||||
* - t-ref
|
||||
* - t-transition
|
||||
* - t-component/t-keepalive
|
||||
* - t-mounted
|
||||
* - t-slot
|
||||
* - t-model
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-on
|
||||
//------------------------------------------------------------------------------
|
||||
// these are pieces of code that will be injected into the event handler if
|
||||
// modifiers are specified
|
||||
const MODS_CODE = {
|
||||
prevent: "e.preventDefault();",
|
||||
self: "if (e.target !== this.elm) {return}",
|
||||
stop: "e.stopPropagation();"
|
||||
};
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "on",
|
||||
priority: 90,
|
||||
atNodeCreation({ ctx, fullName, value, nodeID }) {
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
const [eventName, ...mods] = fullName.slice(5).split(".");
|
||||
if (!eventName) {
|
||||
throw new Error("Missing event name with t-on directive");
|
||||
}
|
||||
let extraArgs;
|
||||
let handlerName = value.replace(/\(.*\)/, function(args) {
|
||||
extraArgs = args.slice(1, -1);
|
||||
return "";
|
||||
});
|
||||
ctx.addIf(`!context['${handlerName}']`);
|
||||
ctx.addLine(
|
||||
`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(
|
||||
/`/g,
|
||||
"'"
|
||||
)}'\`)`
|
||||
);
|
||||
ctx.closeIf();
|
||||
let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
|
||||
let handler;
|
||||
if (mods.length > 0) {
|
||||
handler = `function (e) {`;
|
||||
handler += mods
|
||||
.map(function(mod) {
|
||||
return MODS_CODE[mod];
|
||||
})
|
||||
.join("");
|
||||
handler += `context['${handlerName}'].call(${params}, e);}`;
|
||||
} else {
|
||||
handler = `context['${handlerName}'].bind(${params})`;
|
||||
}
|
||||
if (extraArgs) {
|
||||
ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`);
|
||||
} else {
|
||||
ctx.addLine(
|
||||
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`
|
||||
);
|
||||
ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-ref
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "ref",
|
||||
priority: 95,
|
||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||
const refKey = `ref${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
||||
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-transition
|
||||
//------------------------------------------------------------------------------
|
||||
UTILS.nextFrame = function(cb: () => void) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(cb));
|
||||
};
|
||||
|
||||
UTILS.transitionInsert = function(vn: VNode, name: string) {
|
||||
const elm = <HTMLElement>vn.elm;
|
||||
// remove potential duplicated vnode that is currently being removed, to
|
||||
// prevent from having twice the same node in the DOM during an animation
|
||||
const dup = elm.parentElement && elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
|
||||
if (dup) {
|
||||
dup.remove();
|
||||
}
|
||||
|
||||
elm.classList.add(name + "-enter");
|
||||
elm.classList.add(name + "-enter-active");
|
||||
const finalize = () => {
|
||||
elm.classList.remove(name + "-enter-active");
|
||||
elm.classList.remove(name + "-enter-to");
|
||||
};
|
||||
this.nextFrame(() => {
|
||||
elm.classList.remove(name + "-enter");
|
||||
elm.classList.add(name + "-enter-to");
|
||||
whenTransitionEnd(elm, finalize);
|
||||
});
|
||||
};
|
||||
|
||||
UTILS.transitionRemove = function(vn: VNode, name: string, rm: () => void) {
|
||||
const elm = <HTMLElement>vn.elm;
|
||||
elm.setAttribute("data-owl-key", vn.key!);
|
||||
|
||||
elm.classList.add(name + "-leave");
|
||||
elm.classList.add(name + "-leave-active");
|
||||
const finalize = () => {
|
||||
elm.classList.remove(name + "-leave-active");
|
||||
elm.classList.remove(name + "-leave-to");
|
||||
rm();
|
||||
};
|
||||
this.nextFrame(() => {
|
||||
elm.classList.remove(name + "-leave");
|
||||
elm.classList.add(name + "-leave-to");
|
||||
whenTransitionEnd(elm, finalize);
|
||||
});
|
||||
};
|
||||
|
||||
function getTimeout(delays: Array<string>, durations: Array<string>): number {
|
||||
/* istanbul ignore next */
|
||||
while (delays.length < durations.length) {
|
||||
delays = delays.concat(delays);
|
||||
}
|
||||
|
||||
return Math.max.apply(
|
||||
null,
|
||||
durations.map((d, i) => {
|
||||
return toMs(d) + toMs(delays[i]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
|
||||
// in a locale-dependent way, using a comma instead of a dot.
|
||||
// If comma is not replaced with a dot, the input will be rounded down (i.e. acting
|
||||
// as a floor function) causing unexpected behaviors
|
||||
function toMs(s: string): number {
|
||||
return Number(s.slice(0, -1).replace(",", ".")) * 1000;
|
||||
}
|
||||
|
||||
function whenTransitionEnd(elm: HTMLElement, cb) {
|
||||
const styles = window.getComputedStyle(elm);
|
||||
const delays: Array<string> = (styles.transitionDelay || "").split(", ");
|
||||
const durations: Array<string> = (styles.transitionDuration || "").split(", ");
|
||||
const timeout: number = getTimeout(delays, durations);
|
||||
if (timeout > 0) {
|
||||
elm.addEventListener("transitionend", cb, { once: true });
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "transition",
|
||||
priority: 96,
|
||||
atNodeCreation({ value, addNodeHook }) {
|
||||
let name = value;
|
||||
const hooks = {
|
||||
insert: `this.utils.transitionInsert(vn, '${name}');`,
|
||||
remove: `this.utils.transitionRemove(vn, '${name}', rm);`
|
||||
};
|
||||
for (let hookName in hooks) {
|
||||
addNodeHook(hookName, hooks[hookName]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-component
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, {
|
||||
self: "if (e.target !== vn.elm) {return}"
|
||||
});
|
||||
|
||||
/**
|
||||
* The t-component directive is certainly a complicated and hard to maintain piece
|
||||
* of code. To help you, fellow developer, if you have to maintain it, I offer
|
||||
* you this advice: Good luck...
|
||||
*
|
||||
* Since it is not 'direct' code, but rather code that generates other code, it
|
||||
* is not easy to understand. To help you, here is a detailed and commented
|
||||
* explanation of the code generated by the t-component directive for the following
|
||||
* situation:
|
||||
* ```xml
|
||||
* <Child
|
||||
* t-key="'somestring'"
|
||||
* flag="state.flag"
|
||||
* t-transition="fade"/>
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* // we assign utils on top of the function because it will be useful for
|
||||
* // each components
|
||||
* let utils = this.utils;
|
||||
*
|
||||
* // this is the virtual node representing the parent div
|
||||
* let c1 = [], p1 = { key: 1 };
|
||||
* var vn1 = h("div", p1, c1);
|
||||
*
|
||||
* // t-component directive: we start by evaluating the expression given by t-key:
|
||||
* let key5 = "somestring";
|
||||
*
|
||||
* // def3 is the promise that will contain later either the new component
|
||||
* // creation, or the props update...
|
||||
* let def3;
|
||||
*
|
||||
* // this is kind of tricky: we need here to find if the component was already
|
||||
* // created by a previous rendering. This is done by checking the internal
|
||||
* // `cmap` (children map) of the parent component: it maps keys to component ids,
|
||||
* // and, then, if there is an id, we look into the children list to get the
|
||||
* // instance
|
||||
* let w4 =
|
||||
* key5 in context.__owl__.cmap
|
||||
* ? context.__owl__.children[context.__owl__.cmap[key5]]
|
||||
* : false;
|
||||
*
|
||||
* // We keep the index of the position of the component in the closure. We push
|
||||
* // null to reserve the slot, and will replace it later by the component vnode,
|
||||
* // when it will be ready (do not forget that preparing/rendering a component is
|
||||
* // asynchronous)
|
||||
* let _2_index = c1.length;
|
||||
* c1.push(null);
|
||||
*
|
||||
* // we evaluate here the props given to the component. It is done here to be
|
||||
* // able to easily reference it later, and also, it might be an expensive
|
||||
* // computation, so it is certainly better to do it only once
|
||||
* let props4 = { flag: context["state"].flag };
|
||||
*
|
||||
* // If we have a component, currently rendering, but not ready yet, we do not want
|
||||
* // to wait for it to be ready if we can avoid it
|
||||
* if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
* // we check if the props are the same. In that case, we can simply reuse
|
||||
* // the previous rendering and skip all useless work
|
||||
* if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
* def3 = w4.__owl__.renderPromise;
|
||||
* } else {
|
||||
* // if the props are not the same, we destroy the component and starts anew.
|
||||
* // this will be faster than waiting for its rendering, then updating it
|
||||
* w4.destroy();
|
||||
* w4 = false;
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* if (!w4) {
|
||||
* // in this situation, we need to create a new component. First step is
|
||||
* // to get a reference to the class, then create an instance with
|
||||
* // current context as parent, and the props.
|
||||
* let W4 = context.component && context.components[componentKey4] || QWeb.component[componentKey4];
|
||||
|
||||
* if (!W4) {
|
||||
* throw new Error("Cannot find the definition of component 'child'");
|
||||
* }
|
||||
* w4 = new W4(owner, props4);
|
||||
*
|
||||
* // Whenever we rerender the parent component, we need to be sure that we
|
||||
* // are able to find the component instance. To do that, we register it to
|
||||
* // the parent cmap (children map). Note that the 'template' key is
|
||||
* // used here, since this is what identify the component from the template
|
||||
* // perspective.
|
||||
* context.__owl__.cmap[key5] = w4.__owl__.id;
|
||||
*
|
||||
* // __prepare is called, to basically call willStart, then render the
|
||||
* // component
|
||||
* def3 = w4.__prepare();
|
||||
*
|
||||
* def3 = def3.then(vnode => {
|
||||
* // we create here a virtual node for the parent (NOT the component). This
|
||||
* // means that the vdom of the parent will be stopped here, and from
|
||||
* // the parent's perspective, it simply is a vnode with no children.
|
||||
* // However, it shares the same dom element with the component root
|
||||
* // vnode.
|
||||
* let pvnode = h(vnode.sel, { key: key5 });
|
||||
*
|
||||
* // we add hooks to the parent vnode so we can interact with the new
|
||||
* // component at the proper time
|
||||
* pvnode.data.hook = {
|
||||
* insert(vn) {
|
||||
* // the __mount method will patch the component vdom into the elm vn.elm,
|
||||
* // then call the mounted hooks. However, suprisingly, the snabbdom
|
||||
* // patch method actually replace the elm by a new elm, so we need
|
||||
* // to synchronise the pvnode elm with the resulting elm
|
||||
* let nvn = w4.__mount(vnode, vn.elm);
|
||||
* pvnode.elm = nvn.elm;
|
||||
* // what follows is only present if there are animations on the component
|
||||
* utils.transitionInsert(vn, "fade");
|
||||
* },
|
||||
* remove() {
|
||||
* // override with empty function to prevent from removing the node
|
||||
* // directly. It will be removed when destroy is called anyway, which
|
||||
* // delays the removal if there are animations.
|
||||
* },
|
||||
* destroy() {
|
||||
* // if there are animations, we delay the call to destroy on the
|
||||
* // component, if not, we call it directly.
|
||||
* let finalize = () => {
|
||||
* w4.destroy();
|
||||
* };
|
||||
* utils.transitionRemove(vn, "fade", finalize);
|
||||
* }
|
||||
* };
|
||||
* // the pvnode is inserted at the correct position in the div's children
|
||||
* c1[_2_index] = pvnode;
|
||||
*
|
||||
* // we keep here a reference to the parent vnode (representing the
|
||||
* // component, so we can reuse it later whenever we update the component
|
||||
* w4.__owl__.pvnode = pvnode;
|
||||
* });
|
||||
* } else {
|
||||
* // this is the 'update' path of the directive.
|
||||
* // the call to __updateProps is the actual component update
|
||||
* // Note that we only update the props if we cannot reuse the previous
|
||||
* // rendering work (in the case it was rendered with the same props)
|
||||
* def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
* def3 = def3.then(() => {
|
||||
* // if component was destroyed in the meantime, we do nothing (so, this
|
||||
* // means that the parent's element children list will have a null in
|
||||
* // the component's position, which will cause the pvnode to be removed
|
||||
* // when it is patched.
|
||||
* if (w4.__owl__.isDestroyed) {
|
||||
* return;
|
||||
* }
|
||||
* // like above, we register the pvnode to the children list, so it
|
||||
* // will not be patched out of the dom.
|
||||
* let pvnode = w4.__owl__.pvnode;
|
||||
* c1[_2_index] = pvnode;
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* // we register the deferred here so the parent can coordinate its patch operation
|
||||
* // with all the children.
|
||||
* extra.promises.push(def3);
|
||||
* return vn1;
|
||||
* ```
|
||||
*/
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "component",
|
||||
extraNames: ["props", "keepalive", "asyncroot"],
|
||||
priority: 100,
|
||||
atNodeEncounter({ ctx, value, node, qweb }): boolean {
|
||||
ctx.addLine("//COMPONENT");
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
ctx.rootContext.shouldDefineQWeb = true;
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
|
||||
let async = node.getAttribute("t-asyncroot") ? true : false;
|
||||
|
||||
// t-on- events and t-transition
|
||||
const events: [string, string[], string, string][] = [];
|
||||
let transition: string = "";
|
||||
const attributes = (<Element>node).attributes;
|
||||
const props: { [key: string]: string } = {};
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const name = attributes[i].name;
|
||||
const value = attributes[i].textContent!;
|
||||
if (name.startsWith("t-on-")) {
|
||||
const [eventName, ...mods] = name.slice(5).split(".");
|
||||
let extraArgs;
|
||||
let handlerName = value.replace(/\(.*\)/, function(args) {
|
||||
extraArgs = args.slice(1, -1);
|
||||
return "";
|
||||
});
|
||||
events.push([eventName, mods, handlerName, extraArgs]);
|
||||
} else if (name === "t-transition") {
|
||||
transition = value;
|
||||
} else if (!name.startsWith("t-")) {
|
||||
if (name !== "class" && name !== "style") {
|
||||
// this is a prop!
|
||||
props[name] = ctx.formatExpression(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let key = node.getAttribute("t-key");
|
||||
if (key) {
|
||||
key = ctx.formatExpression(key);
|
||||
}
|
||||
|
||||
// computing the props string representing the props object
|
||||
let propStr = Object.keys(props)
|
||||
.map(k => k + ":" + props[k])
|
||||
.join(",");
|
||||
let dummyID = ctx.generateID();
|
||||
let defID = ctx.generateID();
|
||||
let componentID = ctx.generateID();
|
||||
let keyID = key && ctx.generateID();
|
||||
if (key) {
|
||||
// we bind a variable to the key (could be a complex expression, so we
|
||||
// want to evaluate it only once)
|
||||
ctx.addLine(`let key${keyID} = ${key};`);
|
||||
}
|
||||
ctx.addLine(`let def${defID};`);
|
||||
let templateID = key
|
||||
? `key${keyID}`
|
||||
: ctx.inLoop
|
||||
? `String(-${componentID} - i)`
|
||||
: String(componentID);
|
||||
if (ctx.allowMultipleRoots) {
|
||||
// necessary to prevent collisions
|
||||
if (!key && ctx.inLoop) {
|
||||
let id = ctx.generateID();
|
||||
ctx.addLine(`let template${id} = "_slot_" + String(-${componentID} - i)`);
|
||||
templateID = `template${id}`;
|
||||
} else {
|
||||
templateID = `"_slot_${templateID}"`;
|
||||
}
|
||||
}
|
||||
|
||||
let ref = node.getAttribute("t-ref");
|
||||
let refExpr = "";
|
||||
let refKey: string = "";
|
||||
if (ref) {
|
||||
refKey = `ref${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
|
||||
refExpr = `context.refs[${refKey}] = w${componentID};`;
|
||||
}
|
||||
let transitionsInsertCode = "";
|
||||
if (transition) {
|
||||
transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`;
|
||||
}
|
||||
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
|
||||
if (ref && !keepAlive) {
|
||||
finalizeComponentCode += `delete context.refs[${refKey}];`;
|
||||
}
|
||||
if (transition) {
|
||||
finalizeComponentCode = `let finalize = () => {
|
||||
${finalizeComponentCode}
|
||||
};
|
||||
utils.transitionRemove(vn, '${transition}', finalize);`;
|
||||
}
|
||||
|
||||
let createHook = "";
|
||||
let classAttr = node.getAttribute("class");
|
||||
let tattClass = node.getAttribute("t-att-class");
|
||||
let styleAttr = node.getAttribute("style");
|
||||
let tattStyle = node.getAttribute("t-att-style");
|
||||
if (tattStyle) {
|
||||
const attVar = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
|
||||
tattStyle = attVar;
|
||||
}
|
||||
let classObj = "";
|
||||
if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
|
||||
if (classAttr) {
|
||||
let classDef = classAttr
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map(a => `'${a}':true`)
|
||||
.join(",");
|
||||
classObj = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${classObj} = {${classDef}};`);
|
||||
}
|
||||
if (tattClass) {
|
||||
let tattExpr = ctx.formatExpression(tattClass);
|
||||
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
|
||||
tattExpr = `this.utils.toObj(${tattExpr})`;
|
||||
}
|
||||
if (classAttr) {
|
||||
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
|
||||
} else {
|
||||
classObj = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${classObj} = ${tattExpr};`);
|
||||
}
|
||||
}
|
||||
let eventsCode = events
|
||||
.map(function([eventName, mods, handlerName, extraArgs]) {
|
||||
let params = "owner";
|
||||
if (extraArgs) {
|
||||
if (ctx.inLoop) {
|
||||
let argId = ctx.generateID();
|
||||
// we need to evaluate the arguments now, because the handler will
|
||||
// be set asynchronously later when the widget is ready, and the
|
||||
// context might be different.
|
||||
ctx.addLine(`let arg${argId} = ${ctx.formatExpression(extraArgs)};`);
|
||||
params = `owner, arg${argId}`;
|
||||
} else {
|
||||
params = `owner, ${ctx.formatExpression(extraArgs)}`;
|
||||
}
|
||||
}
|
||||
let handler;
|
||||
if (mods.length > 0) {
|
||||
handler = `function (e) {`;
|
||||
handler += mods
|
||||
.map(function(mod) {
|
||||
return T_COMPONENT_MODS_CODE[mod];
|
||||
})
|
||||
.join("");
|
||||
handler += `owner['${handlerName}'].call(${params}, e);}`;
|
||||
} else {
|
||||
handler = `owner['${handlerName}'].bind(${params})`;
|
||||
}
|
||||
return `vn.elm.addEventListener('${eventName}', ${handler});`;
|
||||
})
|
||||
.join("");
|
||||
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
|
||||
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
|
||||
createHook = `vnode.data.hook = {create(_, vn){${styleCode}${eventsCode}}};`;
|
||||
}
|
||||
|
||||
ctx.addLine(
|
||||
`let w${componentID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`
|
||||
);
|
||||
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
|
||||
if (async) {
|
||||
ctx.addLine(`const patchQueue${componentID} = [];`);
|
||||
ctx.addLine(
|
||||
`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
|
||||
);
|
||||
} else {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(null);`);
|
||||
}
|
||||
ctx.addLine(`let props${componentID} = {${propStr}};`);
|
||||
ctx.addIf(
|
||||
`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`
|
||||
);
|
||||
ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.renderProps)`);
|
||||
ctx.addLine(`def${defID} = w${componentID}.__owl__.renderPromise;`);
|
||||
ctx.addElse();
|
||||
ctx.addLine(`w${componentID}.destroy();`);
|
||||
ctx.addLine(`w${componentID} = false;`);
|
||||
ctx.closeIf();
|
||||
ctx.closeIf();
|
||||
|
||||
ctx.addIf(`!w${componentID}`);
|
||||
// new component
|
||||
ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`);
|
||||
ctx.addLine(
|
||||
`let W${componentID} = context.components && context.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
|
||||
);
|
||||
|
||||
// maybe only do this in dev mode...
|
||||
ctx.addLine(
|
||||
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
|
||||
);
|
||||
ctx.addLine(`w${componentID} = new W${componentID}(owner, props${componentID});`);
|
||||
ctx.addLine(`context.__owl__.cmap[${templateID}] = w${componentID}.__owl__.id;`);
|
||||
|
||||
// SLOTS
|
||||
if (node.childNodes.length) {
|
||||
const clone = <Element>node.cloneNode(true);
|
||||
const slotNodes = clone.querySelectorAll("[t-set]");
|
||||
const slotId = qweb.nextSlotId++;
|
||||
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
|
||||
if (slotNodes.length) {
|
||||
for (let i = 0, length = slotNodes.length; i < length; i++) {
|
||||
const slotNode = slotNodes[i];
|
||||
slotNode.parentElement!.removeChild(slotNode);
|
||||
const key = slotNode.getAttribute("t-set")!;
|
||||
slotNode.removeAttribute("t-set");
|
||||
const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx.parentNode!);
|
||||
qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
|
||||
}
|
||||
}
|
||||
if (clone.childNodes.length) {
|
||||
const t = clone.ownerDocument!.createElement("t");
|
||||
for (let child of Object.values(clone.childNodes)) {
|
||||
t.appendChild(child);
|
||||
}
|
||||
const slotFn = qweb._compile(`slot_default_template`, t, ctx.parentNode!);
|
||||
qweb.slots[`${slotId}_default`] = slotFn.bind(qweb);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.addLine(`def${defID} = w${componentID}.__prepare();`);
|
||||
// hack: specify empty remove hook to prevent the node from being removed from the DOM
|
||||
ctx.addLine(
|
||||
`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});c${
|
||||
ctx.parentNode
|
||||
}[_${dummyID}_index]=pvnode;w${componentID}.__owl__.pvnode = pvnode;});`
|
||||
);
|
||||
|
||||
ctx.addElse();
|
||||
// need to update component
|
||||
const patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
|
||||
ctx.addLine(
|
||||
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode});`
|
||||
);
|
||||
let keepAliveCode = "";
|
||||
if (keepAlive) {
|
||||
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`;
|
||||
}
|
||||
ctx.addLine(
|
||||
`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${
|
||||
tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""
|
||||
}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}c${
|
||||
ctx.parentNode
|
||||
}[_${dummyID}_index]=pvnode;});`
|
||||
);
|
||||
ctx.closeIf();
|
||||
|
||||
if (classObj) {
|
||||
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
|
||||
}
|
||||
|
||||
if (async) {
|
||||
ctx.addLine(
|
||||
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`
|
||||
);
|
||||
} else {
|
||||
ctx.addLine(`extra.promises.push(def${defID});`);
|
||||
}
|
||||
|
||||
if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
|
||||
ctx.closeIf();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-mounted
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "mounted",
|
||||
priority: 97,
|
||||
atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) {
|
||||
ctx.rootContext.shouldDefineOwner = true;
|
||||
const eventName = fullName.slice(5);
|
||||
if (!eventName) {
|
||||
throw new Error("Missing event name with t-on directive");
|
||||
}
|
||||
let extraArgs;
|
||||
let handler = value.replace(/\(.*\)/, function(args) {
|
||||
extraArgs = args.slice(1, -1);
|
||||
return "";
|
||||
});
|
||||
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
|
||||
/`/g,
|
||||
"'"
|
||||
)}'\`)})()`;
|
||||
if (extraArgs) {
|
||||
ctx.addLine(
|
||||
`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
|
||||
extraArgs
|
||||
)});`
|
||||
);
|
||||
} else {
|
||||
ctx.addLine(
|
||||
`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
|
||||
);
|
||||
}
|
||||
addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-slot
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "slot",
|
||||
priority: 80,
|
||||
atNodeEncounter({ ctx, value }): boolean {
|
||||
const slotKey = ctx.generateID();
|
||||
ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`);
|
||||
ctx.addIf(`slot${slotKey}`);
|
||||
ctx.addLine(
|
||||
`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${
|
||||
ctx.parentNode
|
||||
}}));`
|
||||
);
|
||||
ctx.closeIf();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-model
|
||||
//------------------------------------------------------------------------------
|
||||
UTILS.toNumber = function(val: string): number | string {
|
||||
const n = parseFloat(val);
|
||||
return isNaN(n) ? val : n;
|
||||
};
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "model",
|
||||
priority: 42,
|
||||
atNodeCreation({ ctx, nodeID, value, node, fullName }) {
|
||||
const type = node.getAttribute("type");
|
||||
let handler;
|
||||
let event = fullName.includes(".lazy") ? "change" : "input";
|
||||
if (node.tagName === "select") {
|
||||
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
|
||||
event = "change";
|
||||
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
|
||||
} else if (type === "checkbox") {
|
||||
ctx.addLine(`p${nodeID}.props = {checked: context.state['${value}']};`);
|
||||
handler = `(ev) => {context.state['${value}'] = ev.target.checked}`;
|
||||
} else if (type === "radio") {
|
||||
const nodeValue = node.getAttribute("value")!;
|
||||
ctx.addLine(`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`);
|
||||
handler = `(ev) => {context.state['${value}'] = ev.target.value}`;
|
||||
event = "click";
|
||||
} else {
|
||||
ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`);
|
||||
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
|
||||
let valueCode = `ev.target.value${trimCode}`;
|
||||
if (fullName.includes(".number")) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
valueCode = `utils.toNumber(${valueCode})`;
|
||||
}
|
||||
handler = `(ev) => {context.state['${value}'] = ${valueCode}}`;
|
||||
}
|
||||
ctx.addLine(
|
||||
`extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});`
|
||||
);
|
||||
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`);
|
||||
}
|
||||
});
|
||||
+188
-83
@@ -1,16 +1,36 @@
|
||||
import { Component } from "./component";
|
||||
import { Component, Env } from "./component";
|
||||
import { EventBus } from "./event_bus";
|
||||
import { Observer } from "./observer";
|
||||
|
||||
/**
|
||||
* Owl Store
|
||||
*
|
||||
* We have here:
|
||||
* - a Store class
|
||||
* - a connect function
|
||||
*
|
||||
* The Owl store is our answer to the problem of managing complex state across
|
||||
* components. The main idea is that the store owns some state, allow external
|
||||
* code to modify it through actions/mutations, and for each state changes,
|
||||
* connected component will be notified, and updated if necessary.
|
||||
*
|
||||
* Note that this code is partly inspired by VueX and React/Redux
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Store Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
type Mutation = ({ state, commit, getters }, ...payload: any) => void;
|
||||
type Action = ({ commit, state, dispatch, env, getters }, ...payload: any) => void;
|
||||
type Getter = ({ state, getters }, payload) => any;
|
||||
|
||||
interface StoreConfig {
|
||||
env?: any;
|
||||
env?: Env;
|
||||
state?: any;
|
||||
actions?: any;
|
||||
mutations?: { [name: string]: any };
|
||||
actions?: { [name: string]: Action };
|
||||
getters?: { [name: string]: Getter };
|
||||
mutations?: { [name: string]: Mutation };
|
||||
}
|
||||
|
||||
interface StoreOption {
|
||||
@@ -26,7 +46,9 @@ export class Store extends EventBus {
|
||||
debug: boolean;
|
||||
env: any;
|
||||
observer: Observer;
|
||||
set: any;
|
||||
getters: { [name: string]: (payload?) => any };
|
||||
_gettersCache: { [name: string]: {} };
|
||||
_updateId: number = 1;
|
||||
|
||||
constructor(config: StoreConfig, options: StoreOption = {}) {
|
||||
super();
|
||||
@@ -36,17 +58,34 @@ export class Store extends EventBus {
|
||||
this.mutations = config.mutations;
|
||||
this.env = config.env;
|
||||
this.observer = new Observer();
|
||||
this.observer.notifyCB = this.trigger.bind(this, "update");
|
||||
this.observer.notifyCB = this.__notifyComponents.bind(this);
|
||||
this.observer.allowMutations = false;
|
||||
this.observer.observe(this.state);
|
||||
this.getters = {};
|
||||
this._gettersCache = {};
|
||||
|
||||
if (this.debug) {
|
||||
this.history.push({ state: this.state });
|
||||
}
|
||||
this.set = this.observer.set.bind(this.observer);
|
||||
|
||||
const cTypes = ["undefined", "number", "string"];
|
||||
for (let entry of Object.entries(config.getters || {})) {
|
||||
const name: string = entry[0];
|
||||
const func: (...any) => any = entry[1];
|
||||
this.getters[name] = payload => {
|
||||
if (this._commitLevel === 0 && cTypes.indexOf(typeof payload) >= 0) {
|
||||
this._gettersCache[name] = this._gettersCache[name] || {};
|
||||
this._gettersCache[name][payload] =
|
||||
this._gettersCache[name][payload] ||
|
||||
func({ state: this.state, getters: this.getters }, payload);
|
||||
return this._gettersCache[name][payload];
|
||||
}
|
||||
return func({ state: this.state, getters: this.getters }, payload);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(action, payload?: any): Promise<void> | void {
|
||||
dispatch(action: string, ...payload: any): Promise<void> | void {
|
||||
if (!this.actions[action]) {
|
||||
throw new Error(`[Error] action ${action} is undefined`);
|
||||
}
|
||||
@@ -55,9 +94,10 @@ export class Store extends EventBus {
|
||||
commit: this.commit.bind(this),
|
||||
dispatch: this.dispatch.bind(this),
|
||||
env: this.env,
|
||||
state: this.state
|
||||
state: this.state,
|
||||
getters: this.getters
|
||||
},
|
||||
payload
|
||||
...payload
|
||||
);
|
||||
if (result instanceof Promise) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -67,7 +107,7 @@ export class Store extends EventBus {
|
||||
}
|
||||
}
|
||||
|
||||
commit(type, payload?: any) {
|
||||
commit(type: string, ...payload: any): any {
|
||||
if (!this.mutations[type]) {
|
||||
throw new Error(`[Error] mutation ${type} is undefined`);
|
||||
}
|
||||
@@ -79,9 +119,9 @@ export class Store extends EventBus {
|
||||
{
|
||||
commit: this.commit.bind(this),
|
||||
state: this.state,
|
||||
set: this.set
|
||||
getters: this.getters
|
||||
},
|
||||
payload
|
||||
...payload
|
||||
);
|
||||
|
||||
if (this._commitLevel === 1) {
|
||||
@@ -90,13 +130,41 @@ export class Store extends EventBus {
|
||||
this.history.push({
|
||||
state: this.state,
|
||||
mutation: type,
|
||||
payload: payload
|
||||
payload: [...payload]
|
||||
});
|
||||
}
|
||||
}
|
||||
this._commitLevel--;
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instead of using trigger to emit an update event, we actually implement
|
||||
* our own function to do that. The reason is that we need to be smarter than
|
||||
* a simple trigger function: we need to wait for parent components to be
|
||||
* done before doing children components. The reason is that if an update
|
||||
* as an effect of destroying a children, we do not want to call the
|
||||
* mapStoreToProps function of the child, nor rendering it.
|
||||
*
|
||||
* This method is not optimal if we have a bunch of asynchronous components:
|
||||
* we wait sequentially for each component to be completed before updating the
|
||||
* next. However, the only things that matters is that children are updated
|
||||
* after their parents. So, this could be optimized by being smarter, and
|
||||
* updating all widgets concurrently, except for parents/children.
|
||||
*/
|
||||
async __notifyComponents() {
|
||||
this._updateId++;
|
||||
const current = this._updateId;
|
||||
this._gettersCache = {};
|
||||
const subs = this.subscriptions.update || [];
|
||||
for (let i = 0, iLen = subs.length; i < iLen; i++) {
|
||||
const sub = subs[i];
|
||||
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
|
||||
if (shouldCallback) {
|
||||
await sub.callback.call(sub.owner, current);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -117,8 +185,24 @@ function deepRevNumber<T extends Object>(o: T): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function connect(mapStateToProps, options: any = {}) {
|
||||
type Constructor<T> = new (...args: any[]) => T;
|
||||
interface EnvWithStore extends Env {
|
||||
store: Store;
|
||||
}
|
||||
type HashFunction = (a: any, b: any) => number;
|
||||
interface StoreOptions {
|
||||
getStore?(Env): Store;
|
||||
hashFunction?: HashFunction;
|
||||
deep?: boolean;
|
||||
}
|
||||
|
||||
export function connect<E extends EnvWithStore, P, S>(
|
||||
Comp: Constructor<Component<E, P, S>>,
|
||||
mapStoreToProps,
|
||||
options: StoreOptions = <StoreOptions>{}
|
||||
) {
|
||||
let hashFunction = options.hashFunction || null;
|
||||
const getStore = options.getStore || (env => env.store);
|
||||
|
||||
if (!hashFunction) {
|
||||
let deep = "deep" in options ? options.deep : true;
|
||||
@@ -144,75 +228,96 @@ export function connect(mapStateToProps, options: any = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
return function(Comp) {
|
||||
return class extends Comp {
|
||||
constructor(parent, props?: any) {
|
||||
const env = parent instanceof Component ? parent.env : parent;
|
||||
const ownProps = Object.assign({}, props || {});
|
||||
const storeProps = mapStateToProps(env.store.state, ownProps);
|
||||
const mergedProps = Object.assign({}, props || {}, storeProps);
|
||||
super(parent, mergedProps);
|
||||
this.__owl__.ownProps = ownProps;
|
||||
this.__owl__.currentStoreProps = storeProps;
|
||||
this.__owl__.storeHash = hashFunction(
|
||||
{
|
||||
state: env.store.state,
|
||||
storeProps: storeProps,
|
||||
revNumber,
|
||||
deepRevNumber
|
||||
},
|
||||
{
|
||||
currentStoreProps: storeProps
|
||||
}
|
||||
);
|
||||
}
|
||||
mounted() {
|
||||
this.env.store.on("update", this, () => {
|
||||
const ownProps = this.__owl__.ownProps;
|
||||
const storeProps = mapStateToProps(this.env.store.state, ownProps);
|
||||
const options: any = {
|
||||
currentStoreProps: this.__owl__.currentStoreProps
|
||||
};
|
||||
const storeHash = hashFunction(
|
||||
{
|
||||
state: this.env.store.state,
|
||||
storeProps: storeProps,
|
||||
revNumber,
|
||||
deepRevNumber
|
||||
},
|
||||
options
|
||||
);
|
||||
let didChange = options.didChange;
|
||||
if (storeHash !== this.__owl__.storeHash) {
|
||||
didChange = true;
|
||||
this.__owl__.storeHash = storeHash;
|
||||
}
|
||||
if (didChange) {
|
||||
this.__owl__.currentStoreProps = storeProps;
|
||||
this.updateProps(ownProps, false);
|
||||
}
|
||||
});
|
||||
super.mounted();
|
||||
}
|
||||
willUnmount() {
|
||||
this.env.store.off("update", this);
|
||||
super.willUnmount();
|
||||
}
|
||||
updateProps(nextProps, forceUpdate) {
|
||||
if (this.__owl__.ownProps !== nextProps) {
|
||||
this.__owl__.currentStoreProps = mapStateToProps(
|
||||
this.env.store.state,
|
||||
nextProps
|
||||
);
|
||||
const Result = class extends Comp {
|
||||
constructor(parent, props?: any) {
|
||||
const env = parent instanceof Component ? parent.env : parent;
|
||||
const store = getStore(env);
|
||||
const ownProps = Object.assign({}, props || {});
|
||||
const storeProps = mapStoreToProps(store.state, ownProps, store.getters);
|
||||
const mergedProps = Object.assign({}, props || {}, storeProps);
|
||||
super(parent, mergedProps);
|
||||
(<any>this.__owl__).ownProps = ownProps;
|
||||
(<any>this.__owl__).currentStoreProps = storeProps;
|
||||
(<any>this.__owl__).store = store;
|
||||
(<any>this.__owl__).storeHash = (<HashFunction>hashFunction)(
|
||||
{
|
||||
state: store.state,
|
||||
storeProps: storeProps,
|
||||
revNumber,
|
||||
deepRevNumber
|
||||
},
|
||||
{
|
||||
currentStoreProps: storeProps
|
||||
}
|
||||
this.__owl__.ownProps = nextProps;
|
||||
const mergedProps = Object.assign(
|
||||
{},
|
||||
nextProps,
|
||||
this.__owl__.currentStoreProps
|
||||
);
|
||||
return super.updateProps(mergedProps, forceUpdate);
|
||||
);
|
||||
}
|
||||
/**
|
||||
* We do not use the mounted hook here for a subtle reason: we want the
|
||||
* updates to be called for the parents before the children. However,
|
||||
* if we use the mounted hook, this will be done in the reverse order.
|
||||
*/
|
||||
__callMounted() {
|
||||
(<any>this.__owl__).store.on("update", this, this.__checkUpdate);
|
||||
super.__callMounted();
|
||||
}
|
||||
willUnmount() {
|
||||
(<any>this.__owl__).store.off("update", this);
|
||||
super.willUnmount();
|
||||
}
|
||||
|
||||
async __checkUpdate(updateId) {
|
||||
if (updateId === (<any>this.__owl__).currentUpdateId) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
const ownProps = (<any>this.__owl__).ownProps;
|
||||
const storeProps = mapStoreToProps(
|
||||
(<any>this.__owl__).store.state,
|
||||
ownProps,
|
||||
(<any>this.__owl__).store.getters
|
||||
);
|
||||
const options: any = {
|
||||
currentStoreProps: (<any>this.__owl__).currentStoreProps
|
||||
};
|
||||
const storeHash = (<HashFunction>hashFunction)(
|
||||
{
|
||||
state: (<any>this.__owl__).store.state,
|
||||
storeProps: storeProps,
|
||||
revNumber,
|
||||
deepRevNumber
|
||||
},
|
||||
options
|
||||
);
|
||||
let didChange = options.didChange;
|
||||
if (storeHash !== (<any>this.__owl__).storeHash) {
|
||||
didChange = true;
|
||||
(<any>this.__owl__).storeHash = storeHash;
|
||||
}
|
||||
if (didChange) {
|
||||
(<any>this.__owl__).currentStoreProps = storeProps;
|
||||
await this.__updateProps(ownProps, false);
|
||||
}
|
||||
}
|
||||
__updateProps(nextProps, forceUpdate, patchQueue?: any[]) {
|
||||
const __owl__ = <any>this.__owl__;
|
||||
__owl__.currentUpdateId = __owl__.store._updateId;
|
||||
if (__owl__.ownProps !== nextProps) {
|
||||
__owl__.currentStoreProps = mapStoreToProps(
|
||||
__owl__.store.state,
|
||||
nextProps,
|
||||
__owl__.store.getters
|
||||
);
|
||||
}
|
||||
__owl__.ownProps = nextProps;
|
||||
const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps);
|
||||
return super.__updateProps(mergedProps, forceUpdate, patchQueue);
|
||||
}
|
||||
};
|
||||
|
||||
// we assign here a unique name to the resulting anonymous class.
|
||||
// this is necessary for Owl to be able to properly deduce templates.
|
||||
// Otherwise, all connected components would have the same name, and then
|
||||
// each component after the first will necessarily have the same template.
|
||||
let name = `Connected${Comp.name}`;
|
||||
Object.defineProperty(Result, "name", { value: name });
|
||||
return Result;
|
||||
}
|
||||
|
||||
+56
-134
@@ -1,3 +1,58 @@
|
||||
/**
|
||||
* Owl Utils
|
||||
*
|
||||
* We have here a small collection of utility functions:
|
||||
*
|
||||
* - whenReady
|
||||
* - loadJS
|
||||
* - loadTemplates
|
||||
* - escape
|
||||
* - debounce
|
||||
*/
|
||||
|
||||
export function whenReady(fn) {
|
||||
return new Promise(function(resolve) {
|
||||
if (document.readyState !== "loading") {
|
||||
resolve();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", resolve, false);
|
||||
}
|
||||
}).then(fn || function() {});
|
||||
}
|
||||
|
||||
const loadedScripts: { [key: string]: Promise<void> } = {};
|
||||
|
||||
export function loadJS(url: string): Promise<void> {
|
||||
if (url in loadedScripts) {
|
||||
return loadedScripts[url];
|
||||
}
|
||||
const promise: Promise<void> = new Promise(function(resolve, reject) {
|
||||
const script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.src = url;
|
||||
script.onload = function() {
|
||||
resolve();
|
||||
};
|
||||
script.onerror = function() {
|
||||
reject(`Error loading file '${url}'`);
|
||||
};
|
||||
const head = document.head || document.getElementsByTagName("head")[0];
|
||||
head.appendChild(script);
|
||||
});
|
||||
loadedScripts[url] = promise;
|
||||
return promise;
|
||||
}
|
||||
|
||||
export async function loadTemplates(url: string): Promise<string> {
|
||||
const result = await fetch(url);
|
||||
if (!result.ok) {
|
||||
throw new Error("Error while fetching xml templates");
|
||||
}
|
||||
let templates = await result.text();
|
||||
templates = templates.replace(/<!--[\s\S]*?-->/g, "");
|
||||
return templates;
|
||||
}
|
||||
|
||||
export function escape(str: string | number | undefined): string {
|
||||
if (str === undefined) {
|
||||
return "";
|
||||
@@ -13,48 +68,6 @@ export function escape(str: string | number | undefined): string {
|
||||
.replace(/`/g, "`");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove trailing and leading spaces
|
||||
*/
|
||||
export function htmlTrim(s: string): string {
|
||||
let result = s.replace(/(^\s+|\s+$)/g, "");
|
||||
if (s[0] === " ") {
|
||||
result = " " + result;
|
||||
}
|
||||
if (result !== " " && s[s.length - 1] === " ") {
|
||||
result = result + " ";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a function that will generate unique id numbers
|
||||
*/
|
||||
export function idGenerator(): () => number {
|
||||
let nextID = 1;
|
||||
return () => nextID++;
|
||||
}
|
||||
|
||||
export type HashFn = (args: any[]) => string;
|
||||
|
||||
export function memoize<R, T extends (...args: any[]) => R>(
|
||||
f: T,
|
||||
hash?: HashFn
|
||||
): T {
|
||||
if (!hash) {
|
||||
hash = args => args.map(a => String(a)).join(",");
|
||||
}
|
||||
let cache: { [key: string]: R } = {};
|
||||
function memoizedFunction(...args: any[]) {
|
||||
let hashValue = hash!(args);
|
||||
if (!(hashValue in cache)) {
|
||||
cache[hashValue] = f(...args);
|
||||
}
|
||||
return cache[hashValue];
|
||||
}
|
||||
return memoizedFunction as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function, that, as long as it continues to be invoked, will not
|
||||
* be triggered. The function will be called after it stops being called for
|
||||
@@ -63,11 +76,7 @@ export function memoize<R, T extends (...args: any[]) => R>(
|
||||
*
|
||||
* Inspired by https://davidwalsh.name/javascript-debounce-function
|
||||
*/
|
||||
export function debounce(
|
||||
func: Function,
|
||||
wait: number,
|
||||
immediate?: boolean
|
||||
): Function {
|
||||
export function debounce(func: Function, wait: number, immediate?: boolean): Function {
|
||||
let timeout;
|
||||
return function(this: any) {
|
||||
const context = this;
|
||||
@@ -86,90 +95,3 @@ export function debounce(
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface Tree<T> {
|
||||
children: T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a node in a tree.
|
||||
*
|
||||
* This will traverse the tree (depth first) and return the first child that
|
||||
* matches the predicate, if any
|
||||
*/
|
||||
export function findInTree<T extends Tree<T>>(
|
||||
tree: T,
|
||||
predicate: (t: T) => boolean
|
||||
): T | null {
|
||||
if (predicate(tree)) {
|
||||
return tree;
|
||||
}
|
||||
for (let child of tree.children) {
|
||||
let match = findInTree(child, predicate);
|
||||
if (match) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function patch(C: any, patchName: string, patch: any) {
|
||||
const proto = C.prototype;
|
||||
if (!proto.__patches) {
|
||||
proto.__patches = {
|
||||
origMethods: {},
|
||||
patches: {},
|
||||
current: []
|
||||
};
|
||||
}
|
||||
if (proto.__patches.patches[patchName]) {
|
||||
throw new Error(`Patch [${patchName}] already exists`);
|
||||
}
|
||||
proto.__patches.patches[patchName] = patch;
|
||||
applyPatch(proto, patch);
|
||||
proto.__patches.current.push(patchName);
|
||||
|
||||
function applyPatch(proto, patch) {
|
||||
Object.keys(patch).forEach(function(methodName) {
|
||||
const method = patch[methodName];
|
||||
if (typeof method === "function") {
|
||||
const original = proto[methodName];
|
||||
if (!(methodName in proto.__patches.origMethods)) {
|
||||
proto.__patches.origMethods[methodName] = original;
|
||||
}
|
||||
proto[methodName] = function(...args) {
|
||||
this._super = original;
|
||||
return method.call(this, ...args);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function unpatch(C: any, patchName: string) {
|
||||
const proto = C.prototype;
|
||||
const patchInfo = proto.__patches;
|
||||
delete proto.__patches;
|
||||
|
||||
// reset to original
|
||||
for (let k in patchInfo.origMethods) {
|
||||
proto[k] = patchInfo.origMethods[k];
|
||||
}
|
||||
|
||||
// apply other patches
|
||||
for (let name of patchInfo.current) {
|
||||
if (name !== patchName) {
|
||||
patch(C, name, patchInfo.patches[name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadTemplates(url: string): Promise<string> {
|
||||
const result = await fetch(url);
|
||||
if (!result.ok) {
|
||||
throw new Error("Error while fetching xml templates");
|
||||
}
|
||||
let templates = await result.text();
|
||||
templates = templates.replace(/<!--[\s\S]*?-->/g, "");
|
||||
return templates;
|
||||
}
|
||||
+98
-131
@@ -1,9 +1,29 @@
|
||||
/**
|
||||
* Owl VDOM
|
||||
*
|
||||
* This file contains an implementation of a virtual DOM, which is a system that
|
||||
* can generate in-memory representations of a DOM tree, compare them, and
|
||||
* eventually change a concrete DOM tree to match its representation, in an
|
||||
* hopefully efficient way.
|
||||
*
|
||||
* Note that this code is a fork of Snabbdom, slightly tweaked/optimized for our
|
||||
* needs (see https://github.com/snabbdom/snabbdom).
|
||||
*
|
||||
* The main exported values are:
|
||||
* - interface VNode
|
||||
* - h function (a helper function to generate a vnode)
|
||||
* - patch function (to apply a vnode to an actual DOM node)
|
||||
*/
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +48,6 @@ interface VNodeData {
|
||||
hook?: Hooks;
|
||||
key?: Key;
|
||||
ns?: string; // for SVGs
|
||||
fn?: () => VNode; // for thunks
|
||||
args?: Array<any>; // for thunks
|
||||
[key: string]: any; // for any other 3rd party module
|
||||
}
|
||||
|
||||
@@ -41,14 +59,7 @@ function vnode(
|
||||
elm: Element | Text | undefined
|
||||
): VNode {
|
||||
let key = data === undefined ? undefined : data.key;
|
||||
return {
|
||||
sel: sel,
|
||||
data: data,
|
||||
children: children,
|
||||
text: text,
|
||||
elm: elm,
|
||||
key: key
|
||||
};
|
||||
return { sel, data, children, text, elm, key };
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -98,14 +109,7 @@ function createKeyToOldIdx(
|
||||
return map;
|
||||
}
|
||||
|
||||
const hooks: (keyof Module)[] = [
|
||||
"create",
|
||||
"update",
|
||||
"remove",
|
||||
"destroy",
|
||||
"pre",
|
||||
"post"
|
||||
];
|
||||
const hooks: (keyof Module)[] = ["create", "update", "remove", "destroy", "pre", "post"];
|
||||
|
||||
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
let i: number,
|
||||
@@ -127,13 +131,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
function emptyNodeAt(elm: Element) {
|
||||
const id = elm.id ? "#" + elm.id : "";
|
||||
const c = elm.className ? "." + elm.className.split(" ").join(".") : "";
|
||||
return vnode(
|
||||
api.tagName(elm).toLowerCase() + id + c,
|
||||
{},
|
||||
[],
|
||||
undefined,
|
||||
elm
|
||||
);
|
||||
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
|
||||
}
|
||||
|
||||
function createRmCb(childElm: Node, listeners: number) {
|
||||
@@ -147,6 +145,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
|
||||
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
|
||||
let i: any,
|
||||
iLen: number,
|
||||
data = vnode.data;
|
||||
if (data !== undefined) {
|
||||
if (isDef((i = data.hook)) && isDef((i = i.init))) {
|
||||
@@ -167,20 +166,16 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
const dotIdx = sel.indexOf(".", hashIdx);
|
||||
const hash = hashIdx > 0 ? hashIdx : sel.length;
|
||||
const dot = dotIdx > 0 ? dotIdx : sel.length;
|
||||
const tag =
|
||||
hashIdx !== -1 || dotIdx !== -1
|
||||
? sel.slice(0, Math.min(hash, dot))
|
||||
: sel;
|
||||
const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
|
||||
const elm = (vnode.elm =
|
||||
isDef(data) && isDef((i = (data as VNodeData).ns))
|
||||
? api.createElementNS(i, tag)
|
||||
: api.createElement(tag));
|
||||
if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
|
||||
if (dotIdx > 0)
|
||||
elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
|
||||
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
|
||||
if (dotIdx > 0) elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
|
||||
for (i = 0, iLen = cbs.create.length; i < iLen; ++i) cbs.create[i](emptyNode, vnode);
|
||||
if (array(children)) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
for (i = 0, iLen = children.length; i < iLen; ++i) {
|
||||
const ch = children[i];
|
||||
if (ch != null) {
|
||||
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
|
||||
@@ -218,13 +213,15 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
|
||||
function invokeDestroyHook(vnode: VNode) {
|
||||
let i: any,
|
||||
iLen: number,
|
||||
j: number,
|
||||
jLen: number,
|
||||
data = vnode.data;
|
||||
if (data !== undefined) {
|
||||
if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode);
|
||||
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
|
||||
for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i) cbs.destroy[i](vnode);
|
||||
if (vnode.children !== undefined) {
|
||||
for (j = 0; j < vnode.children.length; ++j) {
|
||||
for (j = 0, jLen = vnode.children.length; j < jLen; ++j) {
|
||||
i = vnode.children[j];
|
||||
if (i != null && typeof i !== "string") {
|
||||
invokeDestroyHook(i);
|
||||
@@ -242,6 +239,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
): void {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
let i: any,
|
||||
iLen: number,
|
||||
listeners: number,
|
||||
rm: () => void,
|
||||
ch = vnodes[startIdx];
|
||||
@@ -250,12 +248,8 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
invokeDestroyHook(ch);
|
||||
listeners = cbs.remove.length + 1;
|
||||
rm = createRmCb(ch.elm as Node, listeners);
|
||||
for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
|
||||
if (
|
||||
isDef((i = ch.data)) &&
|
||||
isDef((i = i.hook)) &&
|
||||
isDef((i = i.remove))
|
||||
) {
|
||||
for (i = 0, iLen = cbs.remove.length; i < iLen; ++i) cbs.remove[i](ch, rm);
|
||||
if (isDef((i = ch.data)) && isDef((i = i.hook)) && isDef((i = i.remove))) {
|
||||
i(ch, rm);
|
||||
} else {
|
||||
rm();
|
||||
@@ -317,11 +311,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
} else if (sameVnode(oldEndVnode, newStartVnode)) {
|
||||
// Vnode moved left
|
||||
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
|
||||
api.insertBefore(
|
||||
parentElm,
|
||||
oldEndVnode.elm as Node,
|
||||
oldStartVnode.elm as Node
|
||||
);
|
||||
api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else {
|
||||
@@ -348,11 +338,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
} else {
|
||||
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
|
||||
oldCh[idxInOld] = undefined as any;
|
||||
api.insertBefore(
|
||||
parentElm,
|
||||
elmToMove.elm as Node,
|
||||
oldStartVnode.elm as Node
|
||||
);
|
||||
api.insertBefore(parentElm, elmToMove.elm as Node, oldStartVnode.elm as Node);
|
||||
}
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
}
|
||||
@@ -361,31 +347,16 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
|
||||
if (oldStartIdx > oldEndIdx) {
|
||||
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
|
||||
addVnodes(
|
||||
parentElm,
|
||||
before,
|
||||
newCh,
|
||||
newStartIdx,
|
||||
newEndIdx,
|
||||
insertedVnodeQueue
|
||||
);
|
||||
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
|
||||
} else {
|
||||
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function patchVnode(
|
||||
oldVnode: VNode,
|
||||
vnode: VNode,
|
||||
insertedVnodeQueue: VNodeQueue
|
||||
) {
|
||||
let i: any, hook: any;
|
||||
if (
|
||||
isDef((i = vnode.data)) &&
|
||||
isDef((hook = i.hook)) &&
|
||||
isDef((i = hook.prepatch))
|
||||
) {
|
||||
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
|
||||
let i: any, iLen: number, hook: any;
|
||||
if (isDef((i = vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch))) {
|
||||
i(oldVnode, vnode);
|
||||
}
|
||||
const elm = (vnode.elm = oldVnode.elm as Node);
|
||||
@@ -393,19 +364,14 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
let ch = vnode.children;
|
||||
if (oldVnode === vnode) return;
|
||||
if (vnode.data !== undefined) {
|
||||
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
|
||||
for (i = 0, iLen = cbs.update.length; i < iLen; ++i) cbs.update[i](oldVnode, vnode);
|
||||
i = vnode.data.hook;
|
||||
if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
|
||||
}
|
||||
if (isUndef(vnode.text)) {
|
||||
if (isDef(oldCh) && isDef(ch)) {
|
||||
if (oldCh !== ch)
|
||||
updateChildren(
|
||||
elm,
|
||||
oldCh as Array<VNode>,
|
||||
ch as Array<VNode>,
|
||||
insertedVnodeQueue
|
||||
);
|
||||
updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
|
||||
} else if (isDef(ch)) {
|
||||
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
|
||||
addVnodes(
|
||||
@@ -417,23 +383,13 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
insertedVnodeQueue
|
||||
);
|
||||
} else if (isDef(oldCh)) {
|
||||
removeVnodes(
|
||||
elm,
|
||||
oldCh as Array<VNode>,
|
||||
0,
|
||||
(oldCh as Array<VNode>).length - 1
|
||||
);
|
||||
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
|
||||
} else if (isDef(oldVnode.text)) {
|
||||
api.setTextContent(elm, "");
|
||||
}
|
||||
} else if (oldVnode.text !== vnode.text) {
|
||||
if (isDef(oldCh)) {
|
||||
removeVnodes(
|
||||
elm,
|
||||
oldCh as Array<VNode>,
|
||||
0,
|
||||
(oldCh as Array<VNode>).length - 1
|
||||
);
|
||||
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
|
||||
}
|
||||
api.setTextContent(elm, vnode.text as string);
|
||||
}
|
||||
@@ -443,9 +399,9 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
}
|
||||
|
||||
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
|
||||
let i: number, elm: Node, parent: Node;
|
||||
let i: number, iLen: number, elm: Node, parent: Node;
|
||||
const insertedVnodeQueue: VNodeQueue = [];
|
||||
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
|
||||
for (i = 0, iLen = cbs.pre.length; i < iLen; ++i) cbs.pre[i]();
|
||||
|
||||
if (!isVnode(oldVnode)) {
|
||||
oldVnode = emptyNodeAt(oldVnode);
|
||||
@@ -465,12 +421,12 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < insertedVnodeQueue.length; ++i) {
|
||||
for (i = 0, iLen = insertedVnodeQueue.length; i < iLen; ++i) {
|
||||
(((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(
|
||||
insertedVnodeQueue[i]
|
||||
);
|
||||
}
|
||||
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
|
||||
for (i = 0, iLen = cbs.post.length; i < iLen; ++i) cbs.post[i]();
|
||||
return vnode;
|
||||
};
|
||||
}
|
||||
@@ -491,11 +447,7 @@ interface DOMAPI {
|
||||
createElementNS: (namespaceURI: string, qualifiedName: string) => Element;
|
||||
createTextNode: (text: string) => Text;
|
||||
createComment: (text: string) => Comment;
|
||||
insertBefore: (
|
||||
parentNode: Node,
|
||||
newNode: Node,
|
||||
referenceNode: Node | null
|
||||
) => void;
|
||||
insertBefore: (parentNode: Node, newNode: Node, referenceNode: Node | null) => void;
|
||||
removeChild: (node: Node, child: Node) => void;
|
||||
appendChild: (node: Node, child: Node) => void;
|
||||
parentNode: (node: Node) => Node;
|
||||
@@ -524,11 +476,7 @@ function createComment(text: string): Comment {
|
||||
return document.createComment(text);
|
||||
}
|
||||
|
||||
function insertBefore(
|
||||
parentNode: Node,
|
||||
newNode: Node,
|
||||
referenceNode: Node | null
|
||||
): void {
|
||||
function insertBefore(parentNode: Node, newNode: Node, referenceNode: Node | null): void {
|
||||
parentNode.insertBefore(newNode, referenceNode);
|
||||
}
|
||||
|
||||
@@ -625,21 +573,13 @@ type VNodeChildElement = VNode | string | number | undefined | null;
|
||||
type ArrayOrElement<T> = T | T[];
|
||||
type VNodeChildren = ArrayOrElement<VNodeChildElement>;
|
||||
|
||||
function addNS(
|
||||
data: any,
|
||||
children: VNodes | undefined,
|
||||
sel: string | undefined
|
||||
): void {
|
||||
function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
|
||||
data.ns = "http://www.w3.org/2000/svg";
|
||||
if (sel !== "foreignObject" && children !== undefined) {
|
||||
for (let i = 0; i < children.length; ++i) {
|
||||
for (let i = 0, iLen = children.length; i < iLen; ++i) {
|
||||
let childData = children[i].data;
|
||||
if (childData !== undefined) {
|
||||
addNS(
|
||||
childData,
|
||||
(children[i] as VNode).children as VNodes,
|
||||
children[i].sel
|
||||
);
|
||||
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -653,7 +593,8 @@ export function h(sel: any, b?: any, c?: any): VNode {
|
||||
var data: VNodeData = {},
|
||||
children: any,
|
||||
text: any,
|
||||
i: number;
|
||||
i: number,
|
||||
iLen: number;
|
||||
if (c !== undefined) {
|
||||
data = b;
|
||||
if (array(c)) {
|
||||
@@ -675,15 +616,9 @@ export function h(sel: any, b?: any, c?: any): VNode {
|
||||
}
|
||||
}
|
||||
if (children !== undefined) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
for (i = 0, iLen = children.length; i < iLen; ++i) {
|
||||
if (primitive(children[i]))
|
||||
children[i] = vnode(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
children[i],
|
||||
undefined
|
||||
);
|
||||
children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
|
||||
}
|
||||
}
|
||||
if (
|
||||
@@ -749,9 +684,7 @@ interface Module {
|
||||
//------------------------------------------------------------------------------
|
||||
// module/eventlisteners.ts
|
||||
//------------------------------------------------------------------------------
|
||||
type On = {
|
||||
[N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void
|
||||
} & {
|
||||
type On = { [N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void } & {
|
||||
[event: string]: EventListener;
|
||||
};
|
||||
|
||||
@@ -773,7 +706,7 @@ function invokeHandler(handler: any, vnode?: VNode, event?: Event): void {
|
||||
}
|
||||
} else {
|
||||
// call multiple handlers
|
||||
for (var i = 0; i < handler.length; i++) {
|
||||
for (let i = 0, iLen = handler.length; i < iLen; i++) {
|
||||
invokeHandler(handler[i], vnode, event);
|
||||
}
|
||||
}
|
||||
@@ -830,8 +763,7 @@ function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
|
||||
// add new listeners which has not already attached
|
||||
if (on) {
|
||||
// reuse existing listener or create new
|
||||
var listener = ((vnode as any).listener =
|
||||
(oldVnode as any).listener || createListener());
|
||||
var listener = ((vnode as any).listener = (oldVnode as any).listener || createListener());
|
||||
// update vnode for listener
|
||||
listener.vnode = vnode;
|
||||
|
||||
@@ -918,4 +850,39 @@ export const attrsModule = {
|
||||
update: updateAttrs
|
||||
} as Module;
|
||||
|
||||
export const patch = init([eventListenersModule, attrsModule, propsModule]);
|
||||
//------------------------------------------------------------------------------
|
||||
// class.ts
|
||||
//------------------------------------------------------------------------------
|
||||
function updateClass(oldVnode: VNode, vnode: VNode): void {
|
||||
var cur: any,
|
||||
name: string,
|
||||
elm: Element,
|
||||
oldClass = (oldVnode.data as VNodeData).class,
|
||||
klass = (vnode.data as VNodeData).class;
|
||||
|
||||
if (!oldClass && !klass) return;
|
||||
if (oldClass === klass) return;
|
||||
oldClass = oldClass || {};
|
||||
klass = klass || {};
|
||||
|
||||
elm = vnode.elm as Element;
|
||||
|
||||
for (name in oldClass) {
|
||||
if (!klass[name]) {
|
||||
elm.classList.remove(name);
|
||||
}
|
||||
}
|
||||
for (name in klass) {
|
||||
cur = klass[name];
|
||||
if (cur !== oldClass[name]) {
|
||||
(elm.classList as any)[cur ? "add" : "remove"](name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const classModule = { create: updateClass, update: updateClass } as Module;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// patch
|
||||
//------------------------------------------------------------------------------
|
||||
export const patch = init([eventListenersModule, attrsModule, propsModule, classModule]);
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`animations t-transition combined with component 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//COMPONENT
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let componentKey4 = \`Child\`;
|
||||
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4.__prepare();
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
||||
w4.destroy();
|
||||
};
|
||||
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
} else {
|
||||
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`animations t-transition combined with t-component and t-if 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let utils = this.utils;
|
||||
let QWeb = this.constructor;
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
if (context['state'].display) {
|
||||
//COMPONENT
|
||||
let def3;
|
||||
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let props4 = {};
|
||||
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
def3 = w4.__owl__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false;
|
||||
}
|
||||
}
|
||||
if (!w4) {
|
||||
let componentKey4 = \`Child\`;
|
||||
let W4 = context.components && context.components[componentKey4] || QWeb.components[componentKey4];
|
||||
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||
w4 = new W4(owner, props4);
|
||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||
def3 = w4.__prepare();
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
||||
w4.destroy();
|
||||
};
|
||||
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||
} else {
|
||||
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
}
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`animations t-transition with no delay/duration 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('span', p1, c1);
|
||||
p1.hook = {
|
||||
insert: vn => {
|
||||
this.utils.transitionInsert(vn, 'jupiler');
|
||||
},
|
||||
remove: (vn, rm) => {
|
||||
this.utils.transitionRemove(vn, 'jupiler', rm);
|
||||
},
|
||||
};
|
||||
c1.push({text: \`blue\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`animations t-transition, on a simple node (insert) 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
var h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('span', p1, c1);
|
||||
p1.hook = {
|
||||
insert: vn => {
|
||||
this.utils.transitionInsert(vn, 'chimay');
|
||||
},
|
||||
remove: (vn, rm) => {
|
||||
this.utils.transitionRemove(vn, 'chimay', rm);
|
||||
},
|
||||
};
|
||||
c1.push({text: \`blue\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,9 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`connecting a component to store connecting a component to a local store 1`] = `"<div></div>"`;
|
||||
|
||||
exports[`connecting a component to store connecting a component to a local store 2`] = `"<div><span>hello</span></div>"`;
|
||||
|
||||
exports[`connecting a component to store connecting a component works 1`] = `"<div></div>"`;
|
||||
|
||||
exports[`connecting a component to store connecting a component works 2`] = `"<div><span>hello</span></div>"`;
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
import { Component, Env } from "../src/component";
|
||||
import { QWeb } from "../src/qweb_core";
|
||||
import {
|
||||
makeDeferred,
|
||||
makeTestFixture,
|
||||
makeTestEnv,
|
||||
nextTick,
|
||||
patchNextFrame,
|
||||
renderToDOM,
|
||||
unpatchNextFrame
|
||||
} from "./helpers";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// We create before each test:
|
||||
// - fixture: a div, appended to the DOM, intended to be the target of dom
|
||||
// manipulations. Note that it is removed after each test.
|
||||
// - qweb: a new QWeb instance
|
||||
// - env: a WEnv, necessary to create new components
|
||||
// - cssEl: a stylesheet injected into the dom
|
||||
|
||||
let fixture: HTMLElement;
|
||||
let qweb: QWeb;
|
||||
let env: Env;
|
||||
let cssEl: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
env = makeTestEnv();
|
||||
qweb = new QWeb();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
class Widget extends Component<any, any, any> {}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
describe("animations", () => {
|
||||
beforeEach(() => {
|
||||
cssEl = document.createElement("style");
|
||||
let css = `
|
||||
.chimay-enter-active, .chimay-leave-active {
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.1s;
|
||||
}
|
||||
.chimay-enter, .chimay-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
`;
|
||||
cssEl.textContent = css.trim();
|
||||
document.head.appendChild(cssEl);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.head.removeChild(cssEl);
|
||||
unpatchNextFrame();
|
||||
});
|
||||
|
||||
test("t-transition, on a simple node (insert)", async () => {
|
||||
expect.assertions(5);
|
||||
qweb.addTemplate("test", `<span t-transition="chimay">blue</span>`);
|
||||
|
||||
let def = makeDeferred();
|
||||
patchNextFrame(cb => {
|
||||
expect(node.className).toBe("chimay-enter chimay-enter-active");
|
||||
cb();
|
||||
expect(node.className).toBe("chimay-enter-active chimay-enter-to");
|
||||
def.resolve();
|
||||
});
|
||||
let node: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
|
||||
|
||||
expect(node.className).toBe("chimay-enter chimay-enter-active");
|
||||
await def; // wait for the mocked repaint to be done
|
||||
node.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(node.className).toBe("");
|
||||
});
|
||||
|
||||
test("t-transition with no delay/duration", async () => {
|
||||
expect.assertions(4);
|
||||
qweb.addTemplate("test", `<span t-transition="jupiler">blue</span>`);
|
||||
|
||||
let def = makeDeferred();
|
||||
patchNextFrame(cb => {
|
||||
expect(node.className).toBe("jupiler-enter jupiler-enter-active");
|
||||
cb();
|
||||
expect(node.className).toBe("");
|
||||
def.resolve();
|
||||
});
|
||||
let node: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
|
||||
expect(node.className).toBe("jupiler-enter jupiler-enter-active");
|
||||
await def;
|
||||
});
|
||||
|
||||
test("t-transition on a conditional node", async () => {
|
||||
expect.assertions(7);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"TestWidget",
|
||||
`<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = { hide: false };
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
|
||||
// insert widget into the DOM
|
||||
let def = makeDeferred();
|
||||
var spanNode;
|
||||
patchNextFrame(cb => {
|
||||
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
||||
cb();
|
||||
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
|
||||
def.resolve();
|
||||
});
|
||||
await widget.mount(fixture);
|
||||
spanNode = widget.el!.children[0];
|
||||
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(spanNode.className).toBe("");
|
||||
|
||||
// remove span from the DOM
|
||||
def = makeDeferred();
|
||||
widget.state.hide = true;
|
||||
patchNextFrame(cb => {
|
||||
expect(spanNode.className).toBe("chimay-leave chimay-leave-active");
|
||||
cb();
|
||||
expect(spanNode.className).toBe("chimay-leave-active chimay-leave-to");
|
||||
def.resolve();
|
||||
});
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(spanNode.className).toBe("");
|
||||
});
|
||||
|
||||
test("t-transition combined with t-ref", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"TestWidget",
|
||||
`<div><span t-ref="span" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = { hide: false };
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
|
||||
// insert widget into the DOM
|
||||
let def = makeDeferred();
|
||||
var spanNode;
|
||||
patchNextFrame(cb => {
|
||||
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
||||
cb();
|
||||
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
|
||||
def.resolve();
|
||||
});
|
||||
await widget.mount(fixture);
|
||||
spanNode = widget.el!.children[0];
|
||||
expect(widget.refs.span).toBe(spanNode);
|
||||
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(spanNode.className).toBe("");
|
||||
});
|
||||
|
||||
test("t-transition combined with component", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
env.qweb.addTemplate("Parent", `<div><Child t-transition="chimay"/></div>`);
|
||||
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
||||
class Parent extends Widget {
|
||||
components = { Child: Child };
|
||||
}
|
||||
class Child extends Widget {}
|
||||
const widget = new Parent(env);
|
||||
|
||||
let def = makeDeferred();
|
||||
var spanNode;
|
||||
patchNextFrame(cb => {
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
|
||||
);
|
||||
cb();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter-active chimay-enter-to">blue</span></div>'
|
||||
);
|
||||
def.resolve();
|
||||
});
|
||||
await widget.mount(fixture);
|
||||
spanNode = widget.el!.children[0];
|
||||
|
||||
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
|
||||
);
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
|
||||
});
|
||||
|
||||
test("t-transition combined with t-component and t-if", async () => {
|
||||
expect.assertions(8);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`<div><t t-if="state.display" t-component="Child" t-transition="chimay"/></div>`
|
||||
);
|
||||
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
||||
class Parent extends Widget {
|
||||
components = { Child: Child };
|
||||
state = { display: true };
|
||||
}
|
||||
class Child extends Widget {}
|
||||
const widget = new Parent(env);
|
||||
|
||||
let def = makeDeferred();
|
||||
var spanNode;
|
||||
patchNextFrame(cb => {
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
|
||||
);
|
||||
cb();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter-active chimay-enter-to">blue</span></div>'
|
||||
);
|
||||
def.resolve();
|
||||
});
|
||||
await widget.mount(fixture);
|
||||
spanNode = widget.el!.children[0];
|
||||
|
||||
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
|
||||
);
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
|
||||
|
||||
// remove span from the DOM
|
||||
def = makeDeferred();
|
||||
widget.state.display = false;
|
||||
patchNextFrame(cb => {
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="4">blue</span></div>'
|
||||
);
|
||||
cb();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="4">blue</span></div>'
|
||||
);
|
||||
def.resolve();
|
||||
});
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
});
|
||||
|
||||
test("t-transition combined with t-mounted", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"TestWidget",
|
||||
`<div><span t-if="state.flag" t-mounted="f" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = { flag: false };
|
||||
f() {}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
widget.f = jest.fn();
|
||||
await widget.mount(fixture);
|
||||
|
||||
patchNextFrame(cb => cb());
|
||||
expect(widget.f).toHaveBeenCalledTimes(0);
|
||||
|
||||
widget.state.flag = true;
|
||||
await nextTick();
|
||||
expect(widget.f).toHaveBeenCalledTimes(1);
|
||||
unpatchNextFrame();
|
||||
});
|
||||
|
||||
test("t-transition, remove and re-add before transitionend", async () => {
|
||||
expect.assertions(11);
|
||||
|
||||
env.qweb.addTemplates(
|
||||
`<templates>
|
||||
<div t-name="Parent">
|
||||
<button t-on-click="toggle">Toggle</button>
|
||||
<span t-if="state.flag" t-transition="chimay">blue</span>
|
||||
</div>
|
||||
</templates>`
|
||||
);
|
||||
class Parent extends Widget {
|
||||
constructor(parent) {
|
||||
super(parent);
|
||||
this.state = { flag: false };
|
||||
}
|
||||
toggle() {
|
||||
this.state.flag = !this.state.flag;
|
||||
}
|
||||
}
|
||||
|
||||
const widget = new Parent(env);
|
||||
await widget.mount(fixture);
|
||||
let button = widget.el!.querySelector("button");
|
||||
|
||||
let def = makeDeferred();
|
||||
let phase = "enter";
|
||||
patchNextFrame(cb => {
|
||||
let spans = fixture.querySelectorAll("span");
|
||||
expect(spans.length).toBe(1);
|
||||
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
|
||||
cb();
|
||||
expect(spans[0].className).toBe(`chimay-${phase}-active chimay-${phase}-to`);
|
||||
def.resolve();
|
||||
});
|
||||
|
||||
// click display the span
|
||||
button!.click();
|
||||
await def; // wait for the mocked repaint to be done
|
||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
|
||||
|
||||
// click to remove the span, and click again to re-add it before transitionend
|
||||
def = makeDeferred();
|
||||
phase = "leave";
|
||||
button!.click();
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
def = makeDeferred();
|
||||
phase = "enter";
|
||||
button!.click();
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
|
||||
});
|
||||
|
||||
test("t-transition combined with t-component, remove and re-add before transitionend", async () => {
|
||||
expect.assertions(11);
|
||||
|
||||
env.qweb.addTemplates(
|
||||
`<templates>
|
||||
<div t-name="Parent">
|
||||
<button t-on-click="toggle">Toggle</button>
|
||||
<t t-if="state.flag" t-component="Child" t-transition="chimay"/>
|
||||
</div>
|
||||
<span t-name="Child">blue</span>
|
||||
</templates>`
|
||||
);
|
||||
class Child extends Widget {}
|
||||
class Parent extends Widget {
|
||||
components = { Child };
|
||||
constructor(parent) {
|
||||
super(parent);
|
||||
this.state = { flag: false };
|
||||
}
|
||||
toggle() {
|
||||
this.state.flag = !this.state.flag;
|
||||
}
|
||||
}
|
||||
|
||||
const widget = new Parent(env);
|
||||
await widget.mount(fixture);
|
||||
let button = widget.el!.querySelector("button");
|
||||
|
||||
let def = makeDeferred();
|
||||
let phase = "enter";
|
||||
patchNextFrame(cb => {
|
||||
let spans = fixture.querySelectorAll("span");
|
||||
expect(spans.length).toBe(1);
|
||||
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
|
||||
cb();
|
||||
expect(spans[0].className).toBe(`chimay-${phase}-active chimay-${phase}-to`);
|
||||
def.resolve();
|
||||
});
|
||||
|
||||
// click display the span
|
||||
button!.click();
|
||||
await def; // wait for the mocked repaint to be done
|
||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
|
||||
|
||||
// click to remove the span, and click again to re-add it before transitionend
|
||||
def = makeDeferred();
|
||||
phase = "leave";
|
||||
button!.click();
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
def = makeDeferred();
|
||||
phase = "enter";
|
||||
button!.click();
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
|
||||
});
|
||||
});
|
||||
+2209
-350
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Doc Link Checker
|
||||
*
|
||||
* We define here a test to make sure that there are no dead link in the Owl
|
||||
* documentation.
|
||||
*/
|
||||
import * as fs from "fs";
|
||||
|
||||
const LINK_REGEXP = /\[([^\[]+)\]\(([^\)]+)\)/g;
|
||||
const HEADING_REGEXP = /\n(#+\s*)(.*)/g;
|
||||
|
||||
// files to be checked
|
||||
function getFiles(): string[] {
|
||||
const DOCFILES = fs.readdirSync("doc").map(f => `doc/${f}`);
|
||||
const MAINREADME = "README.md";
|
||||
return DOCFILES.concat(MAINREADME);
|
||||
}
|
||||
|
||||
test("All markdown links work", () => {
|
||||
let linkNumber = 0;
|
||||
let invalidLinkNumber = 0;
|
||||
const files = getFiles();
|
||||
const data = readDocData(files);
|
||||
for (let file of data) {
|
||||
for (let link of file.links) {
|
||||
// DEBUG: uncomment next line
|
||||
// console.warn(`Checking "${link.name}" in "${file.name}"`);
|
||||
linkNumber++;
|
||||
if (!isLinkValid(link, file, data)) {
|
||||
console.warn(`Invalid Link: "${link.name}" in "${file.name}"`);
|
||||
invalidLinkNumber++;
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(invalidLinkNumber).toBe(0);
|
||||
expect(linkNumber).toBeGreaterThan(10);
|
||||
});
|
||||
|
||||
interface MarkDownLink {
|
||||
name: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
interface MarkDownSection {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
interface FileData {
|
||||
name: string;
|
||||
links: MarkDownLink[];
|
||||
sections: MarkDownSection[];
|
||||
}
|
||||
|
||||
function isLinkValid(link: MarkDownLink, current: FileData, files: FileData[]): boolean {
|
||||
const parts = link.link.split("#");
|
||||
const currentParts = current.name.split("/");
|
||||
const path = currentParts.length > 1 ? currentParts[0] + "/" : "";
|
||||
const fullName = path + parts[0];
|
||||
if (parts.length === 1) {
|
||||
// no # in url
|
||||
if (parts[0].endsWith(".md")) {
|
||||
// it is a local md file
|
||||
if (!files.find(f => f.name === fullName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const file = parts[0] === "" ? current : files.find(f => f.name === fullName);
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
if (!file.sections.find(s => s.slug === parts[1])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
|
||||
function slugify(str) {
|
||||
const a = "àáäâãåăæçèéëêǵḧìíïîḿńǹñòóöôœøṕŕßśșțùúüûǘẃẍÿź·_,:;";
|
||||
const b = "aaaaaaaaceeeeghiiiimnnnooooooprssstuuuuuwxyz-----";
|
||||
const p = new RegExp(a.split("").join("|"), "g");
|
||||
return str
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace(/\//g, "") // remove /
|
||||
.replace(/\s+/g, "-") // Replace spaces with -
|
||||
.replace(p, c => b.charAt(a.indexOf(c))) // Replace special characters
|
||||
.replace(/&/g, "-and-") // Replace & with ‘and’
|
||||
.replace(/[^\w\-]+/g, "") // Remove all non-word characters
|
||||
.replace(/\-\-+/g, "-") // Replace multiple - with single -
|
||||
.replace(/^-+/, "") // Trim - from start of text
|
||||
.replace(/-+$/, ""); // Trim - from end of text
|
||||
}
|
||||
|
||||
function readDocData(files: string[]): FileData[] {
|
||||
const result: FileData[] = [];
|
||||
|
||||
for (let file of files) {
|
||||
const fileData: FileData = {
|
||||
name: file,
|
||||
links: [],
|
||||
sections: []
|
||||
};
|
||||
const content = fs.readFileSync(file, { encoding: "utf8" });
|
||||
let m;
|
||||
// get links info
|
||||
do {
|
||||
m = LINK_REGEXP.exec(content);
|
||||
if (m) {
|
||||
fileData.links.push({ name: m[0], link: m[2] });
|
||||
}
|
||||
} while (m);
|
||||
// get sections info
|
||||
do {
|
||||
m = HEADING_REGEXP.exec(content);
|
||||
if (m) {
|
||||
fileData.sections.push({ name: m[0], slug: slugify(m[2]) });
|
||||
}
|
||||
} while (m);
|
||||
|
||||
result.push(fileData);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -23,9 +23,7 @@ describe("event bus behaviour", () => {
|
||||
test("throw error if callback is undefined", () => {
|
||||
expect.assertions(1);
|
||||
const bus = new EventBus();
|
||||
expect(() => bus.on("event", {}, <any>undefined)).toThrow(
|
||||
`Missing callback`
|
||||
);
|
||||
expect(() => bus.on("event", {}, <any>undefined)).toThrow(`Missing callback`);
|
||||
});
|
||||
|
||||
test("can unsubscribe", () => {
|
||||
|
||||
+56
-2
@@ -1,5 +1,8 @@
|
||||
import { Env } from "../src/component";
|
||||
import { QWeb } from "../src/qweb";
|
||||
import { EvalContext, QWeb, UTILS } from "../src/qweb_core";
|
||||
import { patch } from "../src/vdom";
|
||||
import "../src/qweb_directives";
|
||||
import "../src/qweb_extensions";
|
||||
|
||||
export function nextMicroTick(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
@@ -35,8 +38,59 @@ export function makeDeferred(): Deferred {
|
||||
return <Deferred>def;
|
||||
}
|
||||
|
||||
export function makeTestWEnv(): Env {
|
||||
export function makeTestEnv(): Env {
|
||||
return {
|
||||
qweb: new QWeb()
|
||||
};
|
||||
}
|
||||
|
||||
export function trim(str: string): string {
|
||||
return str.replace(/\s/g, "");
|
||||
}
|
||||
|
||||
export function renderToDOM(
|
||||
qweb: QWeb,
|
||||
template: string,
|
||||
context: EvalContext = {},
|
||||
extra?: any
|
||||
): HTMLElement | Text {
|
||||
const vnode = qweb.render(template, context, extra);
|
||||
|
||||
// we snapshot here the compiled code. This is useful to prevent unwanted code
|
||||
// change.
|
||||
expect(qweb.templates[template].fn.toString()).toMatchSnapshot();
|
||||
|
||||
if (vnode.sel === undefined) {
|
||||
return document.createTextNode(vnode.text!);
|
||||
}
|
||||
const node = document.createElement(vnode.sel!);
|
||||
const result = patch(node, vnode);
|
||||
return result.elm as HTMLElement;
|
||||
}
|
||||
|
||||
export function renderToString(qweb: QWeb, t: string, context: EvalContext = {}): string {
|
||||
const node = renderToDOM(qweb, t, context);
|
||||
return node instanceof Text ? node.textContent! : node.outerHTML;
|
||||
}
|
||||
|
||||
// hereafter, we define two helpers to patch/unpatch the nextFrame utils. This
|
||||
// is useful for animations tests, as we hook before repaints to trigger
|
||||
// animations (thanks to requestAnimationFrame). Patching nextFrame allows to
|
||||
// simulate calls to this hook. One must not forget to unpatch afterwards.
|
||||
let nextFrame = UTILS.nextFrame;
|
||||
export function patchNextFrame(f: Function) {
|
||||
UTILS.nextFrame = (cb: () => void) => {
|
||||
setTimeout(() => f(cb));
|
||||
};
|
||||
}
|
||||
|
||||
export function unpatchNextFrame() {
|
||||
UTILS.nextFrame = nextFrame;
|
||||
}
|
||||
|
||||
export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, value: string) {
|
||||
input.value = value;
|
||||
input.dispatchEvent(new Event("input"));
|
||||
input.dispatchEvent(new Event("change"));
|
||||
return nextTick();
|
||||
}
|
||||
|
||||
+77
-4
@@ -126,12 +126,63 @@ describe("observer", () => {
|
||||
arr.push({ kriek: 5 });
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(arr.__owl__.rev).toBe(2);
|
||||
expect(arr[0].__owl__.rev).toBe(1);
|
||||
expect(arr[0].__owl__.rev).toBe(2);
|
||||
|
||||
arr[0].kriek = 6;
|
||||
expect(observer.rev).toBe(3);
|
||||
expect(arr.__owl__.rev).toBe(2);
|
||||
expect(arr[0].__owl__.rev).toBe(2);
|
||||
expect(arr[0].__owl__.rev).toBe(3);
|
||||
});
|
||||
|
||||
test("set new property on observed object", async () => {
|
||||
const observer = new Observer();
|
||||
observer.notifyCB = jest.fn();
|
||||
const state: any = { a: 1 };
|
||||
observer.observe(state);
|
||||
expect(state.__owl__.rev).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
expect(observer.notifyCB).toBeCalledTimes(0);
|
||||
|
||||
Observer.set(state, "b", 8);
|
||||
await nextMicroTick();
|
||||
expect(state.__owl__.rev).toBe(2);
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(observer.notifyCB).toBeCalledTimes(1);
|
||||
expect(state.b).toBe(8);
|
||||
});
|
||||
|
||||
test("delete property from observed object", async () => {
|
||||
const observer = new Observer();
|
||||
observer.notifyCB = jest.fn();
|
||||
const state: any = { a: 1, b: 8 };
|
||||
observer.observe(state);
|
||||
expect(state.__owl__.rev).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
expect(observer.notifyCB).toBeCalledTimes(0);
|
||||
|
||||
Observer.delete(state, "b");
|
||||
await nextMicroTick();
|
||||
expect(state.__owl__.rev).toBe(2);
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(observer.notifyCB).toBeCalledTimes(1);
|
||||
expect(state).toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
test("set element in observed array", async () => {
|
||||
const observer = new Observer();
|
||||
observer.notifyCB = jest.fn();
|
||||
const state: any = ["a"];
|
||||
observer.observe(state);
|
||||
expect(state.__owl__.rev).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
expect(observer.notifyCB).toBeCalledTimes(0);
|
||||
|
||||
Observer.set(state, 1, "b");
|
||||
await nextMicroTick();
|
||||
expect(state.__owl__.rev).toBe(2);
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(observer.notifyCB).toBeCalledTimes(1);
|
||||
expect(state).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
test("properly observe arrays in object", () => {
|
||||
@@ -180,12 +231,14 @@ describe("observer", () => {
|
||||
obj.a = { b: 2 };
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(obj.__owl__.rev).toBe(2);
|
||||
expect(obj.a.__owl__.rev).toBe(1);
|
||||
|
||||
// we start at 2 because it is the new observer rev number
|
||||
expect(obj.a.__owl__.rev).toBe(2);
|
||||
|
||||
obj.a.b = 3;
|
||||
expect(observer.rev).toBe(3);
|
||||
expect(obj.__owl__.rev).toBe(2);
|
||||
expect(obj.a.__owl__.rev).toBe(2);
|
||||
expect(obj.a.__owl__.rev).toBe(3);
|
||||
});
|
||||
|
||||
test("deep observe misc changes", () => {
|
||||
@@ -228,6 +281,26 @@ describe("observer", () => {
|
||||
expect(obj2.__owl__.rev).toBe(3);
|
||||
});
|
||||
|
||||
test("can set a property more than once", () => {
|
||||
const observer = new Observer();
|
||||
const obj: any = {};
|
||||
|
||||
observer.observe(obj);
|
||||
expect(obj.__owl__.rev).toBe(1);
|
||||
expect(observer.rev).toBe(1);
|
||||
expect(obj.__owl__.deepRev).toBe(1);
|
||||
|
||||
observer.set(obj, "aku", "always finds annoying problems");
|
||||
expect(observer.rev).toBe(2);
|
||||
expect(obj.__owl__.rev).toBe(2);
|
||||
expect(obj.__owl__.deepRev).toBe(2);
|
||||
|
||||
observer.set(obj, "aku", "always finds good problems");
|
||||
expect(observer.rev).toBe(3);
|
||||
expect(obj.__owl__.rev).toBe(3);
|
||||
expect(obj.__owl__.deepRev).toBe(3);
|
||||
});
|
||||
|
||||
test("properly handle swapping elements", () => {
|
||||
const observer = new Observer();
|
||||
const obj: any = { a: { arr: [] }, b: 1 };
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
import { Component, Env } from "../src/component";
|
||||
import { makeTestFixture, makeTestEnv } from "./helpers";
|
||||
import { QWeb } from "../src";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
let fixture: HTMLElement;
|
||||
let env: Env;
|
||||
let dev: boolean = false;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
env = makeTestEnv();
|
||||
dev = QWeb.dev;
|
||||
QWeb.dev = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
QWeb.dev = dev;
|
||||
});
|
||||
|
||||
class Widget extends Component<any, any, any> {}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//------------------------------------------------------------------------------
|
||||
describe("props validation", () => {
|
||||
test("validation is only done in dev mode", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = ["message"];
|
||||
}
|
||||
|
||||
QWeb.dev = true;
|
||||
expect(() => {
|
||||
new TestWidget(env);
|
||||
}).toThrow();
|
||||
|
||||
QWeb.dev = false;
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test("props validation is also done on update props", async () => {
|
||||
expect.assertions(1);
|
||||
class TestWidget extends Widget {
|
||||
static props = ["message"];
|
||||
}
|
||||
const w = new TestWidget(env, { message: "bottle" });
|
||||
try {
|
||||
await w.__updateProps({});
|
||||
} catch (e) {
|
||||
expect(e.message).toBe("Missing props 'message' (component 'TestWidget')");
|
||||
}
|
||||
});
|
||||
|
||||
test("props: list of strings", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = ["message"];
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env);
|
||||
}).toThrow("Missing props 'message' (component 'TestWidget')");
|
||||
});
|
||||
|
||||
test("validate simple types", async () => {
|
||||
const Tests = [
|
||||
{ type: Number, ok: 1, ko: "1" },
|
||||
{ type: Boolean, ok: true, ko: "1" },
|
||||
{ type: String, ok: "1", ko: 1 },
|
||||
{ type: Object, ok: {}, ko: "1" },
|
||||
{ type: Date, ok: new Date(), ko: "1" },
|
||||
{ type: Function, ok: () => {}, ko: "1" }
|
||||
];
|
||||
|
||||
for (let test of Tests) {
|
||||
let TestWidget = class extends Widget {
|
||||
static props = { p: test.type };
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env);
|
||||
}).toThrow("Missing props 'p'");
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: test.ok });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: test.ko });
|
||||
}).toThrow("Props 'p' of invalid type in component");
|
||||
}
|
||||
});
|
||||
|
||||
test("validate simple types, alternate form", async () => {
|
||||
const Tests = [
|
||||
{ type: Number, ok: 1, ko: "1" },
|
||||
{ type: Boolean, ok: true, ko: "1" },
|
||||
{ type: String, ok: "1", ko: 1 },
|
||||
{ type: Object, ok: {}, ko: "1" },
|
||||
{ type: Date, ok: new Date(), ko: "1" },
|
||||
{ type: Function, ok: () => {}, ko: "1" }
|
||||
];
|
||||
|
||||
for (let test of Tests) {
|
||||
let TestWidget = class extends Widget {
|
||||
static props = { p: { type: test.type } };
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env);
|
||||
}).toThrow("Missing props 'p'");
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: test.ok });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: test.ko });
|
||||
}).toThrow("Props 'p' of invalid type in component");
|
||||
}
|
||||
});
|
||||
|
||||
test("can validate a prop with multiple types", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static props = { p: [String, Boolean] };
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: "string" });
|
||||
new TestWidget(env, { p: true });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: 1 });
|
||||
}).toThrow("Props 'p' of invalid type in component");
|
||||
});
|
||||
|
||||
test("can validate an optional props", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static props = { p: { type: String, optional: true } };
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: "hey" });
|
||||
new TestWidget(env, {});
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: 1 });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("can validate an array with given primitive type", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static props = { p: { type: Array, element: String } };
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: [] });
|
||||
new TestWidget(env, { p: ["string"] });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: [1] });
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: ["string", 1] });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("can validate an array with multiple sub element types", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static props = { p: { type: Array, element: [String, Boolean] } };
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: [] });
|
||||
new TestWidget(env, { p: ["string"] });
|
||||
new TestWidget(env, { p: [false, true, "string"] });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: [true, 1] });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("can validate an object with simple shape", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static props = {
|
||||
p: { type: Object, shape: { id: Number, url: String } }
|
||||
};
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: { id: 1, url: "url" } });
|
||||
new TestWidget(env, { p: { id: 1, url: "url", extra: true } });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: { id: "1", url: "url" } });
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: { id: 1 } });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("can validate recursively complicated prop def", async () => {
|
||||
let TestWidget = class extends Widget {
|
||||
static props = {
|
||||
p: {
|
||||
type: Object,
|
||||
shape: {
|
||||
id: Number,
|
||||
url: [Boolean, { type: Array, element: Number }]
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: { id: 1, url: true } });
|
||||
new TestWidget(env, { p: { id: 1, url: [12] } });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
new TestWidget(env, { p: { id: 1, url: [12, true] } });
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("default props", () => {
|
||||
test("can set default values", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static defaultProps = { p: 4 };
|
||||
}
|
||||
|
||||
const w = new TestWidget(env, {});
|
||||
expect(w.props.p).toBe(4);
|
||||
});
|
||||
|
||||
test("default values are also set whenever component is updated", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static defaultProps = { p: 4 };
|
||||
}
|
||||
|
||||
const w = new TestWidget(env, { p: 1 });
|
||||
await w.__updateProps({});
|
||||
expect(w.props.p).toBe(4);
|
||||
});
|
||||
});
|
||||
+250
-173
@@ -1,6 +1,5 @@
|
||||
import { patch } from "../src/vdom";
|
||||
import { EvalContext, QWeb } from "../src/qweb";
|
||||
import { normalize } from "./helpers";
|
||||
import { QWeb } from "../src/qweb_core";
|
||||
import { normalize, renderToDOM, renderToString, trim } from "./helpers";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup and helpers
|
||||
@@ -15,39 +14,6 @@ beforeEach(() => {
|
||||
qweb = new QWeb();
|
||||
});
|
||||
|
||||
function trim(str: string): string {
|
||||
return str.replace(/\s/g, "");
|
||||
}
|
||||
|
||||
function renderToDOM(
|
||||
qweb: QWeb,
|
||||
template: string,
|
||||
context: EvalContext = {},
|
||||
extra?: any
|
||||
): HTMLElement | Text {
|
||||
const vnode = qweb.render(template, context, extra);
|
||||
|
||||
// we snapshot here the compiled code. This is useful to prevent unwanted code
|
||||
// change.
|
||||
expect(qweb.templates[template].toString()).toMatchSnapshot();
|
||||
|
||||
if (vnode.sel === undefined) {
|
||||
return document.createTextNode(vnode.text!);
|
||||
}
|
||||
const node = document.createElement(vnode.sel!);
|
||||
const result = patch(node, vnode);
|
||||
return result.elm as HTMLElement;
|
||||
}
|
||||
|
||||
function renderToString(
|
||||
qweb: QWeb,
|
||||
t: string,
|
||||
context: EvalContext = {}
|
||||
): string {
|
||||
const node = renderToDOM(qweb, t, context);
|
||||
return node instanceof Text ? node.textContent! : node.outerHTML;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -58,6 +24,16 @@ describe("static templates", () => {
|
||||
expect(renderToString(qweb, "test")).toBe("hello vdom");
|
||||
});
|
||||
|
||||
test("simple dynamic value", () => {
|
||||
qweb.addTemplate("test", '<t><t t-esc="text"/></t>');
|
||||
expect(renderToString(qweb, "test", { text: "hello vdom" })).toBe("hello vdom");
|
||||
});
|
||||
|
||||
test("simple string, with some dynamic value", () => {
|
||||
qweb.addTemplate("test", '<t>hello <t t-esc="text"/></t>');
|
||||
expect(renderToString(qweb, "test", { text: "vdom" })).toBe("hello vdom");
|
||||
});
|
||||
|
||||
test("empty div", () => {
|
||||
qweb.addTemplate("test", "<div></div>");
|
||||
expect(renderToString(qweb, "test")).toBe("<div></div>");
|
||||
@@ -76,9 +52,7 @@ describe("static templates", () => {
|
||||
|
||||
describe("error handling", () => {
|
||||
test("invalid xml", () => {
|
||||
expect(() => qweb.addTemplate("test", "<div>")).toThrow(
|
||||
"Invalid XML in template"
|
||||
);
|
||||
expect(() => qweb.addTemplate("test", "<div>")).toThrow("Invalid XML in template");
|
||||
});
|
||||
|
||||
test("template with text node and tag", () => {
|
||||
@@ -95,12 +69,13 @@ describe("error handling", () => {
|
||||
|
||||
test("cannot add twice the same template", () => {
|
||||
qweb.addTemplate("test", `<t></t>`);
|
||||
expect(() => qweb.addTemplate("test", "<div/>", true)).not.toThrow("already defined");
|
||||
expect(() => qweb.addTemplate("test", "<div/>")).toThrow("already defined");
|
||||
});
|
||||
|
||||
test("loadTemplates throw if parser error", () => {
|
||||
expect(() => {
|
||||
qweb.loadTemplates("<templates><abc>></templates>");
|
||||
qweb.addTemplates("<templates><abc>></templates>");
|
||||
}).toThrow("Invalid XML in template");
|
||||
});
|
||||
|
||||
@@ -113,29 +88,14 @@ describe("error handling", () => {
|
||||
|
||||
test("nice error when t-on is evaluated with a missing event", () => {
|
||||
qweb.addTemplate("templatename", `<div t-on="somemethod"></div>`);
|
||||
expect(() =>
|
||||
qweb.render("templatename", { someMethod() {} }, { handlers: [] })
|
||||
).toThrow("Missing event name with t-on directive");
|
||||
});
|
||||
|
||||
test("error when compiled code is invalid", () => {
|
||||
qweb.addTemplate(
|
||||
"templatename",
|
||||
`<div t-att-hey="}/^function invalid{{>'"></div>`
|
||||
);
|
||||
expect(() => qweb.render("templatename")).toThrow(
|
||||
"Invalid generated code while compiling template 'templatename': Unexpected token }"
|
||||
expect(() => qweb.render("templatename", { someMethod() {} }, { handlers: [] })).toThrow(
|
||||
"Missing event name with t-on directive"
|
||||
);
|
||||
});
|
||||
|
||||
test("error when unknown directive", () => {
|
||||
qweb.addTemplate(
|
||||
"templatename",
|
||||
`<div t-best-beer="rochefort 10">test</div>`
|
||||
);
|
||||
expect(() => qweb.render("templatename")).toThrow(
|
||||
"Unknown QWeb directive: 't-best-beer'"
|
||||
);
|
||||
qweb.addTemplate("templatename", `<div t-best-beer="rochefort 10">test</div>`);
|
||||
expect(() => qweb.render("templatename")).toThrow("Unknown QWeb directive: 't-best-beer'");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -152,9 +112,7 @@ describe("t-esc", () => {
|
||||
|
||||
test.skip("escaping", () => {
|
||||
qweb.addTemplate("test", `<span><t t-esc="var"/></span>`);
|
||||
expect(renderToString(qweb, "test", { var: "<ok>" })).toBe(
|
||||
"<span><ok></span>"
|
||||
);
|
||||
expect(renderToString(qweb, "test", { var: "<ok>" })).toBe("<span><ok></span>");
|
||||
});
|
||||
|
||||
test("escaping on a node", () => {
|
||||
@@ -186,9 +144,7 @@ describe("t-raw", () => {
|
||||
|
||||
test("not escaping", () => {
|
||||
qweb.addTemplate("test", `<div><t t-raw="var"/></div>`);
|
||||
expect(renderToString(qweb, "test", { var: "<ok></ok>" })).toBe(
|
||||
"<div><ok></ok></div>"
|
||||
);
|
||||
expect(renderToString(qweb, "test", { var: "<ok></ok>" })).toBe("<div><ok></ok></div>");
|
||||
});
|
||||
|
||||
test("t-raw and another sibling node", () => {
|
||||
@@ -201,39 +157,28 @@ describe("t-raw", () => {
|
||||
|
||||
describe("t-set", () => {
|
||||
test("set from attribute literal", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div><t t-set="value" t-value="'ok'"/><t t-esc="value"/></div>`
|
||||
);
|
||||
qweb.addTemplate("test", `<div><t t-set="value" t-value="'ok'"/><t t-esc="value"/></div>`);
|
||||
expect(renderToString(qweb, "test")).toBe("<div>ok</div>");
|
||||
});
|
||||
|
||||
test("t-set and t-if", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div >
|
||||
`<div>
|
||||
<t t-set="v" t-value="value"/>
|
||||
<t t-if="v === 'ok'">grimbergen</t>
|
||||
</div>`
|
||||
);
|
||||
expect(renderToString(qweb, "test", { value: "ok" })).toBe(
|
||||
"<div>grimbergen</div>"
|
||||
);
|
||||
expect(renderToString(qweb, "test", { value: "ok" })).toBe("<div>grimbergen</div>");
|
||||
});
|
||||
|
||||
test("set from body literal", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<t><t t-set="value">ok</t><t t-esc="value"/></t>`
|
||||
);
|
||||
qweb.addTemplate("test", `<t><t t-set="value">ok</t><t t-esc="value"/></t>`);
|
||||
expect(renderToString(qweb, "test")).toBe("ok");
|
||||
});
|
||||
|
||||
test("set from attribute lookup", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div><t t-set="stuff" t-value="value"/><t t-esc="stuff"/></div>`
|
||||
);
|
||||
qweb.addTemplate("test", `<div><t t-set="stuff" t-value="value"/><t t-esc="stuff"/></div>`);
|
||||
expect(renderToString(qweb, "test", { value: "ok" })).toBe("<div>ok</div>");
|
||||
});
|
||||
|
||||
@@ -265,18 +210,12 @@ describe("t-set", () => {
|
||||
});
|
||||
|
||||
test("value priority", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div><t t-set="value" t-value="1">2</t><t t-esc="value"/></div>`
|
||||
);
|
||||
qweb.addTemplate("test", `<div><t t-set="value" t-value="1">2</t><t t-esc="value"/></div>`);
|
||||
expect(renderToString(qweb, "test")).toBe("<div>1</div>");
|
||||
});
|
||||
|
||||
test("evaluate value expression", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div><t t-set="value" t-value="1 + 2"/><t t-esc="value"/></div>`
|
||||
);
|
||||
qweb.addTemplate("test", `<div><t t-set="value" t-value="1 + 2"/><t t-esc="value"/></div>`);
|
||||
expect(renderToString(qweb, "test")).toBe("<div>3</div>");
|
||||
});
|
||||
|
||||
@@ -301,25 +240,19 @@ describe("t-set", () => {
|
||||
"test",
|
||||
`<div><t t-set="value" t-value="somevariable + 2"/><t t-esc="value"/></div>`
|
||||
);
|
||||
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe(
|
||||
"<div>45</div>"
|
||||
);
|
||||
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe("<div>45</div>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("t-if", () => {
|
||||
test("boolean value true condition", () => {
|
||||
qweb.addTemplate("test", `<div><t t-if="condition">ok</t></div>`);
|
||||
expect(renderToString(qweb, "test", { condition: true })).toBe(
|
||||
"<div>ok</div>"
|
||||
);
|
||||
expect(renderToString(qweb, "test", { condition: true })).toBe("<div>ok</div>");
|
||||
});
|
||||
|
||||
test("boolean value false condition", () => {
|
||||
qweb.addTemplate("test", `<div><t t-if="condition">ok</t></div>`);
|
||||
expect(renderToString(qweb, "test", { condition: false })).toBe(
|
||||
"<div></div>"
|
||||
);
|
||||
expect(renderToString(qweb, "test", { condition: false })).toBe("<div></div>");
|
||||
});
|
||||
|
||||
test("boolean value condition missing", () => {
|
||||
@@ -336,9 +269,7 @@ describe("t-if", () => {
|
||||
<t t-else="">beer</t></div>
|
||||
`
|
||||
);
|
||||
expect(renderToString(qweb, "test", { color: "red" })).toBe(
|
||||
"<div>red is dead</div>"
|
||||
);
|
||||
expect(renderToString(qweb, "test", { color: "red" })).toBe("<div>red is dead</div>");
|
||||
});
|
||||
|
||||
test("boolean value condition else", () => {
|
||||
@@ -364,9 +295,7 @@ describe("t-if", () => {
|
||||
`
|
||||
);
|
||||
const result = trim(renderToString(qweb, "test", { condition: false }));
|
||||
expect(result).toBe(
|
||||
"<div><span>begin</span>fail-else<span>end</span></div>"
|
||||
);
|
||||
expect(result).toBe("<div><span>begin</span>fail-else<span>end</span></div>");
|
||||
});
|
||||
|
||||
test("can use some boolean operators in expressions", () => {
|
||||
@@ -391,9 +320,7 @@ describe("t-if", () => {
|
||||
m: 5,
|
||||
n: 2
|
||||
};
|
||||
expect(normalize(renderToString(qweb, "test", context))).toBe(
|
||||
"<div>andormgtnlt</div>"
|
||||
);
|
||||
expect(normalize(renderToString(qweb, "test", context))).toBe("<div>andormgtnlt</div>");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -511,11 +438,14 @@ describe("attributes", () => {
|
||||
expect(result).toBe(`<div foo="42"></div>`);
|
||||
});
|
||||
|
||||
test("format expression, other format", () => {
|
||||
qweb.addTemplate("test", `<div t-attf-foo="{{value + 37}}"/>`);
|
||||
const result = renderToString(qweb, "test", { value: 5 });
|
||||
expect(result).toBe(`<div foo="42"></div>`);
|
||||
});
|
||||
|
||||
test("format multiple", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>`
|
||||
);
|
||||
qweb.addTemplate("test", `<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>`);
|
||||
const result = renderToString(qweb, "test", {
|
||||
value1: 0,
|
||||
value2: 1,
|
||||
@@ -550,11 +480,14 @@ describe("attributes", () => {
|
||||
expect(result).toBe(`<div class="hello world"></div>`);
|
||||
});
|
||||
|
||||
test("class and t-attf-class with ternary operation", () => {
|
||||
qweb.addTemplate("test", `<div class="hello" t-attf-class="{{value ? 'world' : ''}}"/>`);
|
||||
const result = renderToString(qweb, "test", { value: true });
|
||||
expect(result).toBe(`<div class="hello world"></div>`);
|
||||
});
|
||||
|
||||
test("t-att-class with object", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div class="static" t-att-class="{a: b, c: d, e: f}"/>`
|
||||
);
|
||||
qweb.addTemplate("test", `<div class="static" t-att-class="{a: b, c: d, e: f}"/>`);
|
||||
const result = renderToString(qweb, "test", { b: true, d: false, f: true });
|
||||
expect(result).toBe(`<div class="static a e"></div>`);
|
||||
});
|
||||
@@ -583,10 +516,7 @@ describe("t-call (template calling", () => {
|
||||
|
||||
test("with unused setbody", () => {
|
||||
qweb.addTemplate("_basic-callee", "<div>ok</div>");
|
||||
qweb.addTemplate(
|
||||
"caller",
|
||||
'<t t-call="_basic-callee"><t t-set="qux" t-value="3"/></t>'
|
||||
);
|
||||
qweb.addTemplate("caller", '<t t-call="_basic-callee"><t t-set="qux" t-value="3"/></t>');
|
||||
const expected = "<div>ok</div>";
|
||||
expect(renderToString(qweb, "caller")).toBe(expected);
|
||||
});
|
||||
@@ -672,25 +602,13 @@ describe("foreach", () => {
|
||||
"test",
|
||||
`
|
||||
<div>
|
||||
<t t-foreach="5" t-as="elem">
|
||||
-<t t-if="elem_first"> first</t><t t-if="elem_last"> last</t> (<t t-esc="elem_parity"/>)
|
||||
<t t-foreach="Array(5)" t-as="elem">
|
||||
-<t t-if="elem_first"> first</t><t t-if="elem_last"> last</t> (<t t-esc="elem_index"/>)
|
||||
</t>
|
||||
</div>`
|
||||
);
|
||||
const result = trim(renderToString(qweb, "test"));
|
||||
const expected = `<div>-first(even)-(odd)-(even)-(odd)-last(even)</div>`;
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test("iterate, integer param", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div><t t-foreach="3" t-as="item">
|
||||
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
|
||||
</t></div>`
|
||||
);
|
||||
const result = trim(renderToString(qweb, "test"));
|
||||
const expected = `<div>[0:00][1:11][2:22]</div>`;
|
||||
const expected = `<div>-first(0)-(1)-(2)-(3)-last(4)</div>`;
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -700,14 +618,12 @@ describe("foreach", () => {
|
||||
`
|
||||
<div>
|
||||
<t t-foreach="value" t-as="item">
|
||||
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/> - <t t-esc="item_parity"/>]
|
||||
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
|
||||
</t>
|
||||
</div>`
|
||||
);
|
||||
const result = trim(
|
||||
renderToString(qweb, "test", { value: { a: 1, b: 2, c: 3 } })
|
||||
);
|
||||
const expected = `<div>[0:a1-even][1:b2-odd][2:c3-even]</div>`;
|
||||
const result = trim(renderToString(qweb, "test", { value: { a: 1, b: 2, c: 3 } }));
|
||||
const expected = `<div>[0:a1][1:b2][2:c3]</div>`;
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -726,23 +642,35 @@ describe("foreach", () => {
|
||||
test("throws error if invalid loop expression", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div><t t-foreach="abc" t-as="item"><span/></t></div>`
|
||||
`<div><t t-foreach="abc" t-as="item"><span t-key="item_index"/></t></div>`
|
||||
);
|
||||
expect(() => qweb.render("test")).toThrow("Invalid loop expression");
|
||||
});
|
||||
|
||||
test("warn if no key in some case", () => {
|
||||
const consoleWarn = console.warn;
|
||||
console.warn = jest.fn();
|
||||
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`
|
||||
<div>
|
||||
<t t-foreach="[1, 2]" t-as="item">
|
||||
<span><t t-esc="item"/></span>
|
||||
</t>
|
||||
</div>`
|
||||
);
|
||||
renderToString(qweb, "test");
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
console.warn = consoleWarn;
|
||||
});
|
||||
});
|
||||
|
||||
describe("misc", () => {
|
||||
test("global", () => {
|
||||
qweb.addTemplate("_callee-asc", `<Año t-att-falló="'agüero'" t-raw="0"/>`);
|
||||
qweb.addTemplate(
|
||||
"_callee-uses-foo",
|
||||
`<span t-esc="foo">foo default</span>`
|
||||
);
|
||||
qweb.addTemplate(
|
||||
"_callee-asc-toto",
|
||||
`<div t-raw="toto">toto default</div>`
|
||||
);
|
||||
qweb.addTemplate("_callee-uses-foo", `<span t-esc="foo">foo default</span>`);
|
||||
qweb.addTemplate("_callee-asc-toto", `<div t-raw="toto">toto default</div>`);
|
||||
qweb.addTemplate(
|
||||
"caller",
|
||||
`
|
||||
@@ -855,10 +783,7 @@ describe("t-on", () => {
|
||||
});
|
||||
|
||||
test("can bind handlers with object arguments", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<button t-on-click="add({val: 5})">Click</button>`
|
||||
);
|
||||
qweb.addTemplate("test", `<button t-on-click="add({val: 5})">Click</button>`);
|
||||
let a = 1;
|
||||
const node = renderToDOM(
|
||||
qweb,
|
||||
@@ -876,10 +801,7 @@ describe("t-on", () => {
|
||||
|
||||
test("can bind handlers with empty object", () => {
|
||||
expect.assertions(2);
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<button t-on-click="doSomething({})">Click</button>`
|
||||
);
|
||||
qweb.addTemplate("test", `<button t-on-click="doSomething({})">Click</button>`);
|
||||
const node = renderToDOM(
|
||||
qweb,
|
||||
"test",
|
||||
@@ -893,12 +815,9 @@ describe("t-on", () => {
|
||||
(<HTMLElement>node).click();
|
||||
});
|
||||
|
||||
test("can bind handlers with empty object (with non empty inner string", () => {
|
||||
test("can bind handlers with empty object (with non empty inner string)", () => {
|
||||
expect.assertions(2);
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<button t-on-click="doSomething({ })">Click</button>`
|
||||
);
|
||||
qweb.addTemplate("test", `<button t-on-click="doSomething({ })">Click</button>`);
|
||||
const node = renderToDOM(
|
||||
qweb,
|
||||
"test",
|
||||
@@ -945,22 +864,149 @@ describe("t-on", () => {
|
||||
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||
(<HTMLElement>node).click();
|
||||
});
|
||||
|
||||
test("t-on with prevent and/or stop modifiers", async () => {
|
||||
expect.assertions(7);
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div>
|
||||
<button t-on-click.prevent="onClickPrevented">Button 1</button>
|
||||
<button t-on-click.stop="onClickStopped">Button 2</button>
|
||||
<button t-on-click.prevent.stop="onClickPreventedAndStopped">Button 3</button>
|
||||
</div>`
|
||||
);
|
||||
let owner = {
|
||||
onClickPrevented(e) {
|
||||
expect(e.defaultPrevented).toBe(true);
|
||||
expect(e.cancelBubble).toBe(false);
|
||||
},
|
||||
onClickStopped(e) {
|
||||
expect(e.defaultPrevented).toBe(false);
|
||||
expect(e.cancelBubble).toBe(true);
|
||||
},
|
||||
onClickPreventedAndStopped(e) {
|
||||
expect(e.defaultPrevented).toBe(true);
|
||||
expect(e.cancelBubble).toBe(true);
|
||||
}
|
||||
};
|
||||
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||
|
||||
const buttons = (<HTMLElement>node).getElementsByTagName("button");
|
||||
buttons[0].click();
|
||||
buttons[1].click();
|
||||
buttons[2].click();
|
||||
});
|
||||
|
||||
test("t-on with self modifier", async () => {
|
||||
expect.assertions(2);
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div>
|
||||
<button t-on-click="onClick"><span>Button</span></button>
|
||||
<button t-on-click.self="onClickSelf"><span>Button</span></button>
|
||||
</div>`
|
||||
);
|
||||
let steps: string[] = [];
|
||||
let owner = {
|
||||
onClick(e) {
|
||||
steps.push("onClick");
|
||||
},
|
||||
onClickSelf(e) {
|
||||
steps.push("onClickSelf");
|
||||
}
|
||||
};
|
||||
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||
|
||||
const buttons = (<HTMLElement>node).getElementsByTagName("button");
|
||||
const spans = (<HTMLElement>node).getElementsByTagName("span");
|
||||
spans[0].click();
|
||||
spans[1].click();
|
||||
buttons[0].click();
|
||||
buttons[1].click();
|
||||
|
||||
expect(steps).toEqual(["onClick", "onClick", "onClickSelf"]);
|
||||
});
|
||||
|
||||
test("t-on with self and prevent modifiers (order matters)", async () => {
|
||||
expect.assertions(2);
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div>
|
||||
<button t-on-click.self.prevent="onClick"><span>Button</span></button>
|
||||
</div>`
|
||||
);
|
||||
let steps: boolean[] = [];
|
||||
let owner = {
|
||||
onClick() {}
|
||||
};
|
||||
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||
(<HTMLElement>node).addEventListener("click", function(e) {
|
||||
steps.push(e.defaultPrevented);
|
||||
});
|
||||
|
||||
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
|
||||
const span = (<HTMLElement>node).getElementsByTagName("span")[0];
|
||||
span.click();
|
||||
button.click();
|
||||
|
||||
expect(steps).toEqual([false, true]);
|
||||
});
|
||||
|
||||
test("t-on with prevent and self modifiers (order matters)", async () => {
|
||||
expect.assertions(2);
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div>
|
||||
<button t-on-click.prevent.self="onClick"><span>Button</span></button>
|
||||
</div>`
|
||||
);
|
||||
let steps: boolean[] = [];
|
||||
let owner = {
|
||||
onClick() {}
|
||||
};
|
||||
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
|
||||
(<HTMLElement>node).addEventListener("click", function(e) {
|
||||
steps.push(e.defaultPrevented);
|
||||
});
|
||||
|
||||
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
|
||||
const span = (<HTMLElement>node).getElementsByTagName("span")[0];
|
||||
span.click();
|
||||
button.click();
|
||||
|
||||
expect(steps).toEqual([true, true]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("t-ref", () => {
|
||||
test("can get a ref on a node", () => {
|
||||
qweb.addTemplate("test", `<div><span t-ref="'myspan'"/></div>`);
|
||||
qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
|
||||
let refs: any = {};
|
||||
renderToDOM(qweb, "test", { refs });
|
||||
expect(refs.myspan.tagName).toBe("SPAN");
|
||||
});
|
||||
|
||||
test("can get a dynamic ref on a node", () => {
|
||||
qweb.addTemplate("test", `<div><span t-ref="'myspan' + 3"/></div>`);
|
||||
qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`);
|
||||
let refs: any = {};
|
||||
renderToDOM(qweb, "test", { refs });
|
||||
renderToDOM(qweb, "test", { refs, id: 3 });
|
||||
expect(refs.myspan3.tagName).toBe("SPAN");
|
||||
});
|
||||
|
||||
test("refs in a loop", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`
|
||||
<div>
|
||||
<t t-foreach="items" t-as="item">
|
||||
<div t-ref="{{item}}" t-key="item"><t t-esc="item"/></div>
|
||||
</t>
|
||||
</div>`
|
||||
);
|
||||
let refs: any = {};
|
||||
renderToDOM(qweb, "test", { refs, items: [1, 2, 3] });
|
||||
expect(Object.keys(refs)).toEqual(["1", "2", "3"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loading templates", () => {
|
||||
@@ -983,15 +1029,15 @@ describe("loading templates", () => {
|
||||
|
||||
<ul t-name="main"><t t-call="items"/></ul>
|
||||
</templates>`;
|
||||
qweb.loadTemplates(data);
|
||||
qweb.addTemplates(data);
|
||||
const result = renderToString(qweb, "main");
|
||||
expect(result).toBe("<ul><li>ok</li><li>foo</li></ul>");
|
||||
});
|
||||
|
||||
test("does not crash if string does not have templates", () => {
|
||||
const data = "";
|
||||
qweb.loadTemplates(data);
|
||||
expect(qweb.processedTemplates).toEqual({});
|
||||
qweb.addTemplates(data);
|
||||
expect(Object.keys(qweb.templates)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1071,13 +1117,10 @@ describe("whitespace handling", () => {
|
||||
|
||||
describe("t-key", () => {
|
||||
test("can use t-key directive on a node", () => {
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div t-key="beer.id"><t t-esc="beer.name"/></div>`
|
||||
qweb.addTemplate("test", `<div t-key="beer.id"><t t-esc="beer.name"/></div>`);
|
||||
expect(renderToString(qweb, "test", { beer: { id: 12, name: "Chimay Rouge" } })).toBe(
|
||||
"<div>Chimay Rouge</div>"
|
||||
);
|
||||
expect(
|
||||
renderToString(qweb, "test", { beer: { id: 12, name: "Chimay Rouge" } })
|
||||
).toBe("<div>Chimay Rouge</div>");
|
||||
});
|
||||
|
||||
test("t-key directive in a list", () => {
|
||||
@@ -1094,3 +1137,37 @@ describe("t-key", () => {
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("debugging", () => {
|
||||
test("t-debug", () => {
|
||||
const consoleLog = console.log;
|
||||
console.log = jest.fn();
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div t-debug="1"><t t-if="true"><span t-debug="1">hey</span></t></div>`
|
||||
);
|
||||
qweb.render("test");
|
||||
expect(qweb.templates.test.fn.toString()).toMatchSnapshot();
|
||||
|
||||
expect(console.log).toHaveBeenCalledTimes(1);
|
||||
console.log = consoleLog;
|
||||
});
|
||||
|
||||
test("t-log", () => {
|
||||
const consoleLog = console.log;
|
||||
console.log = jest.fn();
|
||||
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`<div>
|
||||
<t t-set="foo" t-value="42"/>
|
||||
<t t-log="foo + 3"/>
|
||||
</div>`
|
||||
);
|
||||
qweb.render("test");
|
||||
expect(qweb.templates.test.fn.toString()).toMatchSnapshot();
|
||||
|
||||
expect(console.log).toHaveBeenCalledWith(45);
|
||||
console.log = consoleLog;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { compileExpr, tokenize } from "../src/qweb_expressions";
|
||||
|
||||
describe("tokenizer", () => {
|
||||
test("simple tokens", () => {
|
||||
expect(tokenize("1.3")).toEqual([{ type: "VALUE", value: "1.3" }]);
|
||||
|
||||
expect(tokenize("{}")).toEqual([
|
||||
{ type: "LEFT_BRACE", value: "{" },
|
||||
{ type: "RIGHT_BRACE", value: "}" }
|
||||
]);
|
||||
expect(tokenize("{ }}")).toEqual([
|
||||
{ type: "LEFT_BRACE", value: "{" },
|
||||
{ type: "RIGHT_BRACE", value: "}" },
|
||||
{ type: "RIGHT_BRACE", value: "}" }
|
||||
]);
|
||||
expect(tokenize("a")).toEqual([{ type: "SYMBOL", value: "a" }]);
|
||||
expect(tokenize("true")).toEqual([{ type: "SYMBOL", value: "true" }]);
|
||||
expect(tokenize("abcde")).toEqual([{ type: "SYMBOL", value: "abcde" }]);
|
||||
expect(tokenize("_ab2")).toEqual([{ type: "SYMBOL", value: "_ab2" }]);
|
||||
expect(tokenize("$ab2")).toEqual([{ type: "SYMBOL", value: "$ab2" }]);
|
||||
expect(tokenize("ABC")).toEqual([{ type: "SYMBOL", value: "ABC" }]);
|
||||
|
||||
expect(tokenize("{a: 2}")).toEqual([
|
||||
{ type: "LEFT_BRACE", value: "{" },
|
||||
{ type: "SYMBOL", value: "a" },
|
||||
{ type: "COLON", value: ":" },
|
||||
{ type: "VALUE", value: "2" },
|
||||
{ type: "RIGHT_BRACE", value: "}" }
|
||||
]);
|
||||
expect(tokenize("a,")).toEqual([{ type: "SYMBOL", value: "a" }, { type: "COMMA", value: "," }]);
|
||||
expect(tokenize("][")).toEqual([
|
||||
{ type: "RIGHT_BRACKET", value: "]" },
|
||||
{ type: "LEFT_BRACKET", value: "[" }
|
||||
]);
|
||||
});
|
||||
|
||||
test("various operators", () => {
|
||||
expect(tokenize(">= <= < > !== !=")).toEqual([
|
||||
{ type: "OPERATOR", value: ">=" },
|
||||
{ type: "OPERATOR", value: "<=" },
|
||||
{ type: "OPERATOR", value: "<" },
|
||||
{ type: "OPERATOR", value: ">" },
|
||||
{ type: "OPERATOR", value: "!==" },
|
||||
{ type: "OPERATOR", value: "!=" }
|
||||
]);
|
||||
});
|
||||
|
||||
test("strings", () => {
|
||||
expect(() => tokenize("'")).toThrow("Invalid expression");
|
||||
expect(() => tokenize("'\\")).toThrow("Invalid expression");
|
||||
expect(() => tokenize("'\\'")).toThrow("Invalid expression");
|
||||
expect(tokenize("'hello ged'")).toEqual([{ type: "VALUE", value: "'hello ged'" }]);
|
||||
expect(tokenize("'hello \\'ged\\''")).toEqual([{ type: "VALUE", value: "'hello \\'ged\\''" }]);
|
||||
|
||||
expect(() => tokenize('"')).toThrow("Invalid expression");
|
||||
expect(() => tokenize('"\\"')).toThrow("Invalid expression");
|
||||
expect(tokenize('"hello ged"')).toEqual([{ type: "VALUE", value: '"hello ged"' }]);
|
||||
expect(tokenize('"hello ged"}')).toEqual([
|
||||
{ type: "VALUE", value: '"hello ged"' },
|
||||
{ type: "RIGHT_BRACE", value: "}" }
|
||||
]);
|
||||
expect(tokenize('"hello \\"ged\\""')).toEqual([{ type: "VALUE", value: '"hello \\"ged\\""' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("expression evaluation", () => {
|
||||
test("simple static values", () => {
|
||||
expect(compileExpr("1", {})).toBe("1");
|
||||
expect(compileExpr("1 ", {})).toBe("1");
|
||||
expect(compileExpr("'some string#/, {' ", {})).toBe("'some string#/, {'");
|
||||
expect(compileExpr("{ } ", {})).toBe("{}");
|
||||
expect(compileExpr("{a: 1} ", {})).toBe("{a:1}");
|
||||
expect(compileExpr("{a: 1, b: 2 } ", {})).toBe("{a:1,b:2}");
|
||||
expect(compileExpr("[] ", {})).toBe("[]");
|
||||
expect(compileExpr("[1] ", {})).toBe("[1]");
|
||||
expect(compileExpr("['1', '2'] ", {})).toBe("['1','2']");
|
||||
expect(compileExpr("['1', \"2\"] ", {})).toBe("['1',\"2\"]");
|
||||
});
|
||||
|
||||
test("various types of 'words'", () => {
|
||||
expect(compileExpr("true", {})).toBe("true");
|
||||
expect(compileExpr("false", {})).toBe("false");
|
||||
expect(compileExpr("debugger", {})).toBe("debugger");
|
||||
});
|
||||
|
||||
test("parenthesis", () => {
|
||||
expect(compileExpr("(1)", {})).toBe("(1)");
|
||||
expect(compileExpr("a*(1 +3)", {})).toBe("context['a']*(1+3)");
|
||||
});
|
||||
|
||||
test("objects and sub objects", () => {
|
||||
expect(compileExpr("{a:{b:1}} ", {})).toBe("{a:{b:1}}");
|
||||
});
|
||||
|
||||
test("replacing variables", () => {
|
||||
expect(compileExpr("a", {})).toBe("context['a']");
|
||||
expect(compileExpr("a", { a: { id: "_3", expr: "" } })).toBe("_3");
|
||||
});
|
||||
|
||||
test("arrays and objects", () => {
|
||||
expect(compileExpr("[{b:1}] ", {})).toBe("[{b:1}]");
|
||||
expect(compileExpr("{a: []} ", {})).toBe("{a:[]}");
|
||||
expect(compileExpr("[{b:1, c: [1, {d: {e: 3}} ]}] ", {})).toBe("[{b:1,c:[1,{d:{e:3}}]}]");
|
||||
});
|
||||
|
||||
test("dot operator", () => {
|
||||
expect(compileExpr("a.b", {})).toBe("context['a'].b");
|
||||
expect(compileExpr("a.b.c", {})).toBe("context['a'].b.c");
|
||||
});
|
||||
|
||||
test("various unary operators", () => {
|
||||
expect(compileExpr("!flag", {})).toBe("!context['flag']");
|
||||
expect(compileExpr("-3", {})).toBe("-3");
|
||||
expect(compileExpr("-a", {})).toBe("-context['a']");
|
||||
});
|
||||
|
||||
test("various binary operators", () => {
|
||||
expect(compileExpr("color == 'black'", {})).toBe("context['color']=='black'");
|
||||
expect(compileExpr("a || b", {})).toBe("context['a']||context['b']");
|
||||
expect(compileExpr("color === 'black'", {})).toBe("context['color']==='black'");
|
||||
expect(compileExpr("'li_'+item", {})).toBe("'li_'+context['item']");
|
||||
expect(compileExpr("state.val > 1", {})).toBe("context['state'].val>1");
|
||||
});
|
||||
|
||||
test("boolean operations", () => {
|
||||
expect(compileExpr("a && b", {})).toBe("context['a']&&context['b']");
|
||||
});
|
||||
|
||||
test("ternary operators", () => {
|
||||
expect(compileExpr("a ? b: '2'", {})).toBe("context['a']?context['b']:'2'");
|
||||
expect(compileExpr("a ? b: (c or '2') ", {})).toBe(
|
||||
"context['a']?context['b']:(context['c']||'2')"
|
||||
);
|
||||
expect(compileExpr("a ? {test:c}: [1,u]", {})).toBe(
|
||||
"context['a']?{test:context['c']}:[1,context['u']]"
|
||||
);
|
||||
});
|
||||
|
||||
test("word replacement", () => {
|
||||
expect(compileExpr("a or b", {})).toBe("context['a']||context['b']");
|
||||
expect(compileExpr("a and b", {})).toBe("context['a']&&context['b']");
|
||||
});
|
||||
|
||||
test("function calls", () => {
|
||||
expect(compileExpr("a()", {})).toBe("context['a']()");
|
||||
expect(compileExpr("a(1)", {})).toBe("context['a'](1)");
|
||||
expect(compileExpr("a(1,2)", {})).toBe("context['a'](1,2)");
|
||||
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe("context['a'](1,2,{a:[context['a']]})");
|
||||
expect(compileExpr("'x'.toUpperCase()", {})).toBe("'x'.toUpperCase()");
|
||||
expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe("'x'.toUpperCase({a:3})");
|
||||
expect(compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "" } })).toBe(
|
||||
"'x'.toUpperCase(_v5)"
|
||||
);
|
||||
expect(compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "" } })).toBe(
|
||||
"'x'.toUpperCase({b:_v5})"
|
||||
);
|
||||
});
|
||||
});
|
||||
+839
-122
File diff suppressed because it is too large
Load Diff
+1
-119
@@ -1,13 +1,4 @@
|
||||
import {
|
||||
escape,
|
||||
htmlTrim,
|
||||
idGenerator,
|
||||
memoize,
|
||||
debounce,
|
||||
findInTree,
|
||||
patch,
|
||||
unpatch
|
||||
} from "../src/utils";
|
||||
import { escape, debounce } from "../src/utils";
|
||||
|
||||
describe("escape", () => {
|
||||
test("normal strings", () => {
|
||||
@@ -21,49 +12,6 @@ describe("escape", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("htmlTrim", () => {
|
||||
test("basic use", () => {
|
||||
expect(htmlTrim("abc")).toBe("abc");
|
||||
expect(htmlTrim(" abc")).toBe(" abc");
|
||||
expect(htmlTrim("abc ")).toBe("abc ");
|
||||
expect(htmlTrim(" abc ")).toBe(" abc ");
|
||||
expect(htmlTrim("abc\n ")).toBe("abc ");
|
||||
expect(htmlTrim("\n ")).toBe(" ");
|
||||
expect(htmlTrim(" \n ")).toBe(" ");
|
||||
expect(htmlTrim(" ")).toBe(" ");
|
||||
expect(htmlTrim("")).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("idGenerator", () => {
|
||||
test("basic use", () => {
|
||||
let gen = idGenerator();
|
||||
expect(gen()).toBe(1);
|
||||
expect(gen()).toBe(2);
|
||||
expect(gen()).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("memoize", () => {
|
||||
test("return correct value", () => {
|
||||
const f = memoize((a, b) => a + b);
|
||||
expect(f(1, 3)).toBe(4);
|
||||
});
|
||||
|
||||
test("does not recompute if not needed", () => {
|
||||
let nCalls = 0;
|
||||
function origFunction(a: number, b: number): number {
|
||||
nCalls++;
|
||||
return a + b;
|
||||
}
|
||||
const memoized = memoize(origFunction);
|
||||
|
||||
expect(memoized(1, 3)).toBe(4);
|
||||
expect(memoized(1, 3)).toBe(4);
|
||||
expect(nCalls).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("debounce", () => {
|
||||
test("works as expected", () => {
|
||||
jest.useFakeTimers();
|
||||
@@ -80,69 +28,3 @@ describe("debounce", () => {
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findInTree", () => {
|
||||
test("can find stuff in tree", () => {
|
||||
let tree = {
|
||||
id: 1,
|
||||
children: [{ id: 2, children: [] }, { id: 3, key: "hello", children: [] }]
|
||||
};
|
||||
const match1 = findInTree(tree, t => t.id === 3);
|
||||
expect((<any>match1).key).toBe("hello");
|
||||
const match2 = findInTree(tree, t => t.id === 4);
|
||||
expect(match2).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("patch/unpatch", () => {
|
||||
test("can monkey patch a class", () => {
|
||||
class Test {
|
||||
n = 1;
|
||||
|
||||
doSomething(): string {
|
||||
return "hey";
|
||||
}
|
||||
}
|
||||
|
||||
patch(Test, "some_custo", {
|
||||
doSomething(): string {
|
||||
this.n = this.n + 1;
|
||||
return this._super();
|
||||
}
|
||||
});
|
||||
|
||||
const t = new Test();
|
||||
expect(t.n).toBe(1);
|
||||
expect(t.doSomething()).toBe("hey");
|
||||
expect(t.n).toBe(2);
|
||||
});
|
||||
|
||||
test("cannot patch a class twice with same patch name", () => {
|
||||
class Test {}
|
||||
|
||||
patch(Test, "some_custo", {});
|
||||
expect(() => {
|
||||
patch(Test, "some_custo", {});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("can unpatch a class", () => {
|
||||
class Test {
|
||||
doSomething(): number {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
patch(Test, "some_custo", {
|
||||
doSomething(): number {
|
||||
return this._super() + 2;
|
||||
}
|
||||
});
|
||||
|
||||
const t = new Test();
|
||||
expect(t.doSomething()).toBe(3);
|
||||
|
||||
unpatch(Test, "some_custo");
|
||||
expect(t.doSomething()).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
+28
-143
@@ -59,9 +59,7 @@ describe("attributes", function() {
|
||||
test("are set correctly when namespaced", function() {
|
||||
const vnode1 = h("div", { attrs: { "xlink:href": "#foo" } });
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
expect(elm.getAttributeNS("http://www.w3.org/1999/xlink", "href")).toBe(
|
||||
"#foo"
|
||||
);
|
||||
expect(elm.getAttributeNS("http://www.w3.org/1999/xlink", "href")).toBe("#foo");
|
||||
});
|
||||
|
||||
test("should not touch class nor id fields", function() {
|
||||
@@ -207,12 +205,8 @@ describe("snabbdom", function() {
|
||||
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
|
||||
|
||||
// verify that svg tag automatically gets svg namespace
|
||||
elm = patch(
|
||||
vnode0,
|
||||
h("svg", [
|
||||
h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])
|
||||
])
|
||||
).elm;
|
||||
elm = patch(vnode0, h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]))
|
||||
.elm;
|
||||
expect(elm.namespaceURI).toBe(SVGNamespace);
|
||||
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
|
||||
expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace);
|
||||
@@ -352,13 +346,7 @@ describe("snabbdom", function() {
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
expect(elm.children.length).toBe(2);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
|
||||
});
|
||||
|
||||
test("add elements in the middle", function() {
|
||||
@@ -368,13 +356,7 @@ describe("snabbdom", function() {
|
||||
expect(elm.children.length).toBe(4);
|
||||
expect(elm.children.length).toBe(4);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
|
||||
});
|
||||
|
||||
test("add elements at beginning and end", function() {
|
||||
@@ -383,13 +365,7 @@ describe("snabbdom", function() {
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
expect(elm.children.length).toBe(3);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3", "4", "5"]);
|
||||
});
|
||||
|
||||
test("adds children to parent with no children", function() {
|
||||
@@ -588,35 +564,19 @@ describe("snabbdom", function() {
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
expect(elm.children.length).toBe(6);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"4",
|
||||
"3",
|
||||
"2",
|
||||
"1",
|
||||
"5",
|
||||
"0"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["4", "3", "2", "1", "5", "0"]);
|
||||
});
|
||||
|
||||
test("supports null/undefined children", function() {
|
||||
const vnode1 = h("i", [0, 1, 2, 3, 4, 5].map(spanNum));
|
||||
const vnode2 = h(
|
||||
"i",
|
||||
[null, 2, undefined, null, 1, 0, null, 5, 4, null, 3, undefined].map(
|
||||
spanNum
|
||||
)
|
||||
[null, 2, undefined, null, 1, 0, null, 5, 4, null, 3, undefined].map(spanNum)
|
||||
);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
expect(elm.children.length).toBe(6);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"2",
|
||||
"1",
|
||||
"0",
|
||||
"5",
|
||||
"4",
|
||||
"3"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["2", "1", "0", "5", "4", "3"]);
|
||||
});
|
||||
|
||||
test("supports all null/undefined children", function() {
|
||||
@@ -627,14 +587,7 @@ describe("snabbdom", function() {
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
expect(elm.children.length).toBe(0);
|
||||
elm = patch(vnode2, vnode3).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"5",
|
||||
"4",
|
||||
"3",
|
||||
"2",
|
||||
"1",
|
||||
"0"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["5", "4", "3", "2", "1", "0"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -714,18 +667,10 @@ describe("snabbdom", function() {
|
||||
});
|
||||
|
||||
test("removes elements", function() {
|
||||
const vnode1 = h("div", [
|
||||
h("span", "One"),
|
||||
h("span", "Two"),
|
||||
h("span", "Three")
|
||||
]);
|
||||
const vnode1 = h("div", [h("span", "One"), h("span", "Two"), h("span", "Three")]);
|
||||
const vnode2 = h("div", [h("span", "One"), h("span", "Three")]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"One",
|
||||
"Two",
|
||||
"Three"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Two", "Three"]);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Three"]);
|
||||
});
|
||||
@@ -760,49 +705,19 @@ describe("snabbdom", function() {
|
||||
});
|
||||
|
||||
test("reorders elements", function() {
|
||||
const vnode1 = h("div", [
|
||||
h("span", "One"),
|
||||
h("div", "Two"),
|
||||
h("b", "Three")
|
||||
]);
|
||||
const vnode2 = h("div", [
|
||||
h("b", "Three"),
|
||||
h("span", "One"),
|
||||
h("div", "Two")
|
||||
]);
|
||||
const vnode1 = h("div", [h("span", "One"), h("div", "Two"), h("b", "Three")]);
|
||||
const vnode2 = h("div", [h("b", "Three"), h("span", "One"), h("div", "Two")]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"One",
|
||||
"Two",
|
||||
"Three"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Two", "Three"]);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
expect(map(elm.children, c => c.tagName)).toEqual(["B", "SPAN", "DIV"]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual([
|
||||
"Three",
|
||||
"One",
|
||||
"Two"
|
||||
]);
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["Three", "One", "Two"]);
|
||||
});
|
||||
|
||||
test("supports null/undefined children", function() {
|
||||
const vnode1 = h("i", [null, h("i", "1"), h("i", "2"), null]);
|
||||
const vnode2 = h("i", [
|
||||
h("i", "2"),
|
||||
undefined,
|
||||
undefined,
|
||||
h("i", "1"),
|
||||
undefined
|
||||
]);
|
||||
const vnode3 = h("i", [
|
||||
null,
|
||||
h("i", "1"),
|
||||
undefined,
|
||||
null,
|
||||
h("i", "2"),
|
||||
undefined,
|
||||
null
|
||||
]);
|
||||
const vnode2 = h("i", [h("i", "2"), undefined, undefined, h("i", "1"), undefined]);
|
||||
const vnode3 = h("i", [null, h("i", "1"), undefined, null, h("i", "2"), undefined, null]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2"]);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
@@ -835,10 +750,7 @@ describe("snabbdom", function() {
|
||||
}
|
||||
const vnode1 = h("div", [
|
||||
h("span", "First sibling"),
|
||||
h("div", { hook: { create: cb } }, [
|
||||
h("span", "Child 1"),
|
||||
h("span", "Child 2")
|
||||
]),
|
||||
h("div", { hook: { create: cb } }, [h("span", "Child 1"), h("span", "Child 2")]),
|
||||
h("span", "Can't touch me")
|
||||
]);
|
||||
patch(vnode0, vnode1);
|
||||
@@ -855,10 +767,7 @@ describe("snabbdom", function() {
|
||||
}
|
||||
const vnode1 = h("div", [
|
||||
h("span", "First sibling"),
|
||||
h("div", { hook: { insert: cb } }, [
|
||||
h("span", "Child 1"),
|
||||
h("span", "Child 2")
|
||||
]),
|
||||
h("div", { hook: { insert: cb } }, [h("span", "Child 1"), h("span", "Child 2")]),
|
||||
h("span", "Can touch me")
|
||||
]);
|
||||
patch(vnode0, vnode1);
|
||||
@@ -874,17 +783,11 @@ describe("snabbdom", function() {
|
||||
}
|
||||
const vnode1 = h("div", [
|
||||
h("span", "First sibling"),
|
||||
h("div", { hook: { prepatch: cb } }, [
|
||||
h("span", "Child 1"),
|
||||
h("span", "Child 2")
|
||||
])
|
||||
h("div", { hook: { prepatch: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
|
||||
]);
|
||||
const vnode2 = h("div", [
|
||||
h("span", "First sibling"),
|
||||
h("div", { hook: { prepatch: cb } }, [
|
||||
h("span", "Child 1"),
|
||||
h("span", "Child 2")
|
||||
])
|
||||
h("div", { hook: { prepatch: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
|
||||
]);
|
||||
patch(vnode0, vnode1);
|
||||
patch(vnode1, vnode2);
|
||||
@@ -965,10 +868,7 @@ describe("snabbdom", function() {
|
||||
}
|
||||
const vnode1 = h("div", [
|
||||
h("span", "First sibling"),
|
||||
h("div", { hook: { remove: cb } }, [
|
||||
h("span", "Child 1"),
|
||||
h("span", "Child 2")
|
||||
])
|
||||
h("div", { hook: { remove: cb } }, [h("span", "Child 1"), h("span", "Child 2")])
|
||||
]);
|
||||
const vnode2 = h("div", [h("span", "First sibling")]);
|
||||
patch(vnode0, vnode1);
|
||||
@@ -981,9 +881,7 @@ describe("snabbdom", function() {
|
||||
function cb(vnode) {
|
||||
calls++;
|
||||
}
|
||||
const vnode1 = h("div", [
|
||||
h("div", { hook: { destroy: cb } }, [h("span", "Child 1")])
|
||||
]);
|
||||
const vnode1 = h("div", [h("div", { hook: { destroy: cb } }, [h("span", "Child 1")])]);
|
||||
const vnode2 = h("div", "Text node");
|
||||
patch(vnode0, vnode1);
|
||||
patch(vnode1, vnode2);
|
||||
@@ -1053,10 +951,7 @@ describe("snabbdom", function() {
|
||||
result.push(vnode);
|
||||
rm();
|
||||
}
|
||||
const vnode1 = h("div", { hook: { remove: cb } }, [
|
||||
h("b", "Child 1"),
|
||||
h("i", "Child 2")
|
||||
]);
|
||||
const vnode1 = h("div", { hook: { remove: cb } }, [h("b", "Child 1"), h("i", "Child 2")]);
|
||||
const vnode2 = h("span", [h("b", "Child 1"), h("i", "Child 2")]);
|
||||
patch(vnode0, vnode1);
|
||||
patch(vnode1, vnode2);
|
||||
@@ -1091,10 +986,7 @@ describe("snabbdom", function() {
|
||||
}
|
||||
const vnode1 = h("div", [
|
||||
h("span", "First sibling"),
|
||||
h("div", [
|
||||
h("span", { hook: { destroy: cb } }, "Child 1"),
|
||||
h("span", "Child 2")
|
||||
])
|
||||
h("div", [h("span", { hook: { destroy: cb } }, "Child 1"), h("span", "Child 2")])
|
||||
]);
|
||||
const vnode2 = h("div");
|
||||
patch(vnode0, vnode1);
|
||||
@@ -1150,11 +1042,7 @@ describe("snabbdom", function() {
|
||||
}
|
||||
}
|
||||
]);
|
||||
const vnode1 = h("div", [
|
||||
h("span", "First child"),
|
||||
"",
|
||||
h("span", "Third child")
|
||||
]);
|
||||
const vnode1 = h("div", [h("span", "First child"), "", h("span", "Third child")]);
|
||||
const vnode2 = h("div");
|
||||
patch(vnode0, vnode1);
|
||||
patch(vnode1, vnode2);
|
||||
@@ -1196,10 +1084,7 @@ describe("snabbdom", function() {
|
||||
function cb(vnode) {
|
||||
result.push(vnode);
|
||||
}
|
||||
const vnode1 = h("div", [
|
||||
h("span", { hook: { update: cb } }, "Hello"),
|
||||
h("span", "there")
|
||||
]);
|
||||
const vnode1 = h("div", [h("span", { hook: { update: cb } }, "Hello"), h("span", "there")]);
|
||||
patch(vnode0, vnode1);
|
||||
patch(vnode1, vnode1);
|
||||
expect(result).toHaveLength(0);
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
odoo.define("app", function(require) {
|
||||
const Widget = require("web.Widget");
|
||||
var core = require("web.core");
|
||||
var dom = require("web.dom");
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//----------------------------------------------------------------------------
|
||||
var Counter = Widget.extend({
|
||||
template: "counter",
|
||||
events: {
|
||||
"click .o_increment": "_onIncrement"
|
||||
},
|
||||
init: function(parent) {
|
||||
this._super(parent);
|
||||
this.value = 0;
|
||||
},
|
||||
start: function() {
|
||||
this.updateCounter();
|
||||
},
|
||||
on_attach_callback: function() {},
|
||||
on_detach_callback: function() {},
|
||||
updateCounter: function() {
|
||||
this.$(".o_increment").html("Value: " + this.value);
|
||||
},
|
||||
_onIncrement: function(ev) {
|
||||
ev.stopPropagation();
|
||||
this.value += 1;
|
||||
this.updateCounter();
|
||||
}
|
||||
});
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//----------------------------------------------------------------------------
|
||||
var Message = Widget.extend({
|
||||
template: "message",
|
||||
events: {
|
||||
"click .remove": "_onRemove"
|
||||
},
|
||||
|
||||
init: function(parent, message) {
|
||||
this._super(parent);
|
||||
this.author = message.author;
|
||||
this.msg = message.msg;
|
||||
this.id = message.id;
|
||||
},
|
||||
willStart: function() {
|
||||
this.counter = new Counter(this);
|
||||
return this.counter.appendTo($("<div>"));
|
||||
},
|
||||
|
||||
start: function() {
|
||||
this.$msg = this.$(".msg");
|
||||
dom.append(this.$el, this.counter.$el, {
|
||||
in_DOM: this.isInDom,
|
||||
callbacks: [{ widget: this.counter }]
|
||||
});
|
||||
},
|
||||
on_attach_callback: function() {
|
||||
this.isInDom = true;
|
||||
if (this.counter) {
|
||||
this.counter.on_attach_callback();
|
||||
}
|
||||
},
|
||||
on_detach_callback: function() {
|
||||
this.isInDom = true;
|
||||
if (this.counter) {
|
||||
this.counter.on_detach_callback();
|
||||
}
|
||||
},
|
||||
|
||||
_onRemove: function() {
|
||||
this.trigger_up("remove_message", { id: this.id });
|
||||
},
|
||||
|
||||
update: function() {
|
||||
this.msg += "!!!";
|
||||
this.$msg.text(this.msg);
|
||||
}
|
||||
});
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//----------------------------------------------------------------------------
|
||||
var App = Widget.extend({
|
||||
template: "root",
|
||||
events: {
|
||||
"click .o_btn_msg.100": function() {
|
||||
this.addMessages(100);
|
||||
},
|
||||
"click .o_btn_msg.1000": function() {
|
||||
this.addMessages(1000);
|
||||
},
|
||||
"click .o_btn_msg.10000": function() {
|
||||
this.addMessages(10000);
|
||||
},
|
||||
"click .o_btn_msg.30000": function() {
|
||||
this.addMessages(30000);
|
||||
},
|
||||
"click .updateSomeMessages": function() {
|
||||
this.updateSomeMessages();
|
||||
},
|
||||
"click .clear": function() {
|
||||
this.clear();
|
||||
},
|
||||
"click .o_multiple": function() {
|
||||
this.multipleFlag = !this.multipleFlag;
|
||||
},
|
||||
"click .o_clear": function() {
|
||||
this.clearFlag = !this.clearFlag;
|
||||
},
|
||||
"click .clear-log": function() {
|
||||
this.$log[0].innerHTML = "";
|
||||
}
|
||||
},
|
||||
custom_events: {
|
||||
remove_message: "_onRemoveMessage"
|
||||
},
|
||||
|
||||
init: function(parent) {
|
||||
this._super(parent);
|
||||
this.widgets = {};
|
||||
this.isInDom = false;
|
||||
this.messageCount = 0;
|
||||
this.multipleFlag = false;
|
||||
this.clearFlag = false;
|
||||
},
|
||||
|
||||
start: function() {
|
||||
this.$content = this.$(".content");
|
||||
this.$msgCount = this.$(".message_count");
|
||||
this.$log = this.$(".log-content");
|
||||
this.log("Benchmarking odoo widgets, 12.0");
|
||||
},
|
||||
on_attach_callback: function() {
|
||||
this.isInDom = true;
|
||||
for (let widget of Object.values(this.widgets)) {
|
||||
if (widget.on_attach_callback) {
|
||||
widget.on_attach_callback();
|
||||
}
|
||||
}
|
||||
},
|
||||
on_detach_callback: function() {
|
||||
this.isInDom = true;
|
||||
for (let widget of Object.values(this.widgets)) {
|
||||
if (widget.on_detach_callback) {
|
||||
widget.on_detach_callback();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateMessageCount() {
|
||||
this.$msgCount.text("Number of msg: " + this.messageCount);
|
||||
},
|
||||
|
||||
addMessages: function(n) {
|
||||
const self = this;
|
||||
this.benchmark("add " + n, () => {
|
||||
const defs = [];
|
||||
const messages = buildData(n);
|
||||
for (let message of messages) {
|
||||
const widget = new Message(this, message);
|
||||
this.widgets[message.id] = widget;
|
||||
defs.push(widget.appendTo("<div>"));
|
||||
}
|
||||
return $.when
|
||||
.apply($, defs)
|
||||
.then(function() {
|
||||
for (let message of messages) {
|
||||
let widget = self.widgets[message.id];
|
||||
dom.append(self.$content, widget.$el, {
|
||||
in_DOM: this.isInDom,
|
||||
callbacks: [{ widget: widget }]
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(function() {
|
||||
self.messageCount += n;
|
||||
self.updateMessageCount();
|
||||
});
|
||||
});
|
||||
},
|
||||
clear: function() {
|
||||
startMeasure("clear");
|
||||
this._clear();
|
||||
stopMeasure(info => {
|
||||
this.log(info.msg);
|
||||
});
|
||||
},
|
||||
|
||||
_clear: function() {
|
||||
this.$content.empty();
|
||||
for (let key in this.widgets) {
|
||||
this.widgets[key].destroy();
|
||||
delete this.widgets[key];
|
||||
}
|
||||
this.messageCount = 0;
|
||||
this.updateMessageCount();
|
||||
},
|
||||
updateSomeMessages: function() {
|
||||
this.benchmark("update every 10th", () => {
|
||||
const widgets = Object.values(this.widgets);
|
||||
for (let i = 0; i < widgets.length; i += 10) {
|
||||
widgets[i].update();
|
||||
}
|
||||
});
|
||||
},
|
||||
_onRemoveMessage: function(ev) {
|
||||
startMeasure("remove message");
|
||||
ev.target.destroy();
|
||||
delete this.widgets[ev.data.id];
|
||||
this.messageCount--;
|
||||
this.updateMessageCount();
|
||||
stopMeasure();
|
||||
},
|
||||
|
||||
benchmark: function(message, fn, callback) {
|
||||
if (this.multipleFlag) {
|
||||
const N = 20;
|
||||
let n = N;
|
||||
let total = 0;
|
||||
let cb = info => {
|
||||
let finalize = () => {
|
||||
n--;
|
||||
total += info.delta;
|
||||
if (n === 0) {
|
||||
const avg = total / N;
|
||||
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
this._benchmark(message, fn, cb);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.clearFlag) {
|
||||
this._benchmark("clear", this._clear.bind(this), finalize, false);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
this._benchmark(message, fn, cb);
|
||||
} else {
|
||||
this._benchmark(message, fn, callback);
|
||||
}
|
||||
},
|
||||
|
||||
_benchmark: function(message, fn, cb, log = true) {
|
||||
setTimeout(() => {
|
||||
startMeasure(message);
|
||||
const benchmark = fn();
|
||||
(benchmark && benchmark.then ? benchmark : $.when()).then(() => {
|
||||
stopMeasure(info => {
|
||||
if (log) {
|
||||
this.log(info.msg);
|
||||
}
|
||||
if (cb) {
|
||||
cb(info);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
});
|
||||
},
|
||||
|
||||
log: function(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.$log[0].appendChild(div);
|
||||
this.$log[0].scrollTop = this.$log[0].scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
// prepare QWeb
|
||||
const templates = await fetch("templates.xml");
|
||||
let strTemplates = await templates.text();
|
||||
strTemplates = strTemplates.replace(/<!--[\s\S]*?-->/g, "");
|
||||
core.qweb.add_template(strTemplates);
|
||||
|
||||
// prepare app
|
||||
const app = new App();
|
||||
await app.appendTo(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Odoo Widget Benchmark (12.0)</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='web.assets_common.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,47 @@
|
||||
<templates>
|
||||
<div class="main" t-name="root">
|
||||
<div class="left-thing">
|
||||
<div class="title">Actions</div>
|
||||
<div class="panel">
|
||||
<button class="o_btn_msg 100">Add 100 messages</button>
|
||||
<button class="o_btn_msg 1000">Add 1k messages</button>
|
||||
<button class="o_btn_msg 10000">Add 10k messages</button>
|
||||
<button class="o_btn_msg 30000">Add 30k messages</button>
|
||||
<button class="updateSomeMessages">Update every 10th message</button>
|
||||
<button class="clear">Clear</button>
|
||||
</div>
|
||||
<div class="flags">
|
||||
<div>
|
||||
<input type="checkbox" class="o_multiple" id="multipleflag" />
|
||||
<label for="multipleflag">Do it 20x</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" class="o_clear" id="clearFlag" />
|
||||
<label for="clearFlag">Clear after</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info message_count">
|
||||
Number of messages: 0
|
||||
</div>
|
||||
<div class="title">Log <span class="clear-log">(clear)</span></div>
|
||||
<div class="log">
|
||||
<div class="log-content"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="widget.author"/></span>
|
||||
<span class="msg"><t t-esc="widget.msg"/></span>
|
||||
<button class="remove">Remove</button>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button class="o_increment"></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,301 @@
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
odoo.define("app", function(require) {
|
||||
const Widget = require("web.Widget");
|
||||
var core = require("web.core");
|
||||
var dom = require("web.dom");
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//----------------------------------------------------------------------------
|
||||
var Counter = Widget.extend({
|
||||
template: "counter",
|
||||
events: {
|
||||
"click .o_increment": "_onIncrement"
|
||||
},
|
||||
init: function(parent) {
|
||||
this._super(parent);
|
||||
this.value = 0;
|
||||
},
|
||||
start: function() {
|
||||
this.updateCounter();
|
||||
},
|
||||
on_attach_callback: function() {},
|
||||
on_detach_callback: function() {},
|
||||
updateCounter: function() {
|
||||
this.$(".o_increment").html("Value: " + this.value);
|
||||
},
|
||||
_onIncrement: function(ev) {
|
||||
ev.stopPropagation();
|
||||
this.value += 1;
|
||||
this.updateCounter();
|
||||
}
|
||||
});
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//----------------------------------------------------------------------------
|
||||
var Message = Widget.extend({
|
||||
template: "message",
|
||||
events: {
|
||||
"click .remove": "_onRemove"
|
||||
},
|
||||
|
||||
init: function(parent, message) {
|
||||
this._super(parent);
|
||||
this.author = message.author;
|
||||
this.msg = message.msg;
|
||||
this.id = message.id;
|
||||
},
|
||||
willStart: function() {
|
||||
this.counter = new Counter(this);
|
||||
return this.counter.appendTo($("<div>"));
|
||||
},
|
||||
|
||||
start: function() {
|
||||
this.$msg = this.$(".msg");
|
||||
dom.append(this.$el, this.counter.$el, {
|
||||
in_DOM: this.isInDom,
|
||||
callbacks: [{ widget: this.counter }]
|
||||
});
|
||||
},
|
||||
on_attach_callback: function() {
|
||||
this.isInDom = true;
|
||||
if (this.counter) {
|
||||
this.counter.on_attach_callback();
|
||||
}
|
||||
},
|
||||
on_detach_callback: function() {
|
||||
this.isInDom = true;
|
||||
if (this.counter) {
|
||||
this.counter.on_detach_callback();
|
||||
}
|
||||
},
|
||||
|
||||
_onRemove: function() {
|
||||
this.trigger_up("remove_message", { id: this.id });
|
||||
},
|
||||
|
||||
update: function() {
|
||||
this.msg += "!!!";
|
||||
this.$msg.text(this.msg);
|
||||
}
|
||||
});
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//----------------------------------------------------------------------------
|
||||
var App = Widget.extend({
|
||||
template: "root",
|
||||
events: {
|
||||
"click .o_btn_msg.100": function() {
|
||||
this.addMessages(100);
|
||||
},
|
||||
"click .o_btn_msg.1000": function() {
|
||||
this.addMessages(1000);
|
||||
},
|
||||
"click .o_btn_msg.10000": function() {
|
||||
this.addMessages(10000);
|
||||
},
|
||||
"click .o_btn_msg.30000": function() {
|
||||
this.addMessages(30000);
|
||||
},
|
||||
"click .updateSomeMessages": function() {
|
||||
this.updateSomeMessages();
|
||||
},
|
||||
"click .clear": function() {
|
||||
this.clear();
|
||||
},
|
||||
"click .o_multiple": function() {
|
||||
this.multipleFlag = !this.multipleFlag;
|
||||
},
|
||||
"click .o_clear": function() {
|
||||
this.clearFlag = !this.clearFlag;
|
||||
},
|
||||
"click .clear-log": function() {
|
||||
this.$log[0].innerHTML = "";
|
||||
}
|
||||
},
|
||||
custom_events: {
|
||||
remove_message: "_onRemoveMessage"
|
||||
},
|
||||
|
||||
init: function(parent) {
|
||||
this._super(parent);
|
||||
this.widgets = {};
|
||||
this.isInDom = false;
|
||||
this.messageCount = 0;
|
||||
this.multipleFlag = false;
|
||||
this.clearFlag = false;
|
||||
},
|
||||
|
||||
start: function() {
|
||||
this.$content = this.$(".content");
|
||||
this.$msgCount = this.$(".message_count");
|
||||
this.$log = this.$(".log-content");
|
||||
this.log("Benchmarking odoo widgets, 12.3");
|
||||
},
|
||||
on_attach_callback: function() {
|
||||
this.isInDom = true;
|
||||
for (let widget of Object.values(this.widgets)) {
|
||||
if (widget.on_attach_callback) {
|
||||
widget.on_attach_callback();
|
||||
}
|
||||
}
|
||||
},
|
||||
on_detach_callback: function() {
|
||||
this.isInDom = true;
|
||||
for (let widget of Object.values(this.widgets)) {
|
||||
if (widget.on_detach_callback) {
|
||||
widget.on_detach_callback();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateMessageCount() {
|
||||
this.$msgCount.text("Number of msg: " + this.messageCount);
|
||||
},
|
||||
|
||||
addMessages: function(n) {
|
||||
const self = this;
|
||||
this.benchmark("add " + n, () => {
|
||||
const defs = [];
|
||||
const messages = buildData(n);
|
||||
for (let message of messages) {
|
||||
const widget = new Message(this, message);
|
||||
this.widgets[message.id] = widget;
|
||||
defs.push(widget.appendTo("<div>"));
|
||||
}
|
||||
return $.when
|
||||
.apply($, defs)
|
||||
.then(function() {
|
||||
for (let message of messages) {
|
||||
let widget = self.widgets[message.id];
|
||||
dom.append(self.$content, widget.$el, {
|
||||
in_DOM: this.isInDom,
|
||||
callbacks: [{ widget: widget }]
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(function() {
|
||||
self.messageCount += n;
|
||||
self.updateMessageCount();
|
||||
});
|
||||
});
|
||||
},
|
||||
clear: function() {
|
||||
startMeasure("clear");
|
||||
this._clear();
|
||||
stopMeasure(info => {
|
||||
this.log(info.msg);
|
||||
});
|
||||
},
|
||||
|
||||
_clear: function() {
|
||||
this.$content.empty();
|
||||
for (let key in this.widgets) {
|
||||
this.widgets[key].destroy();
|
||||
delete this.widgets[key];
|
||||
}
|
||||
this.messageCount = 0;
|
||||
this.updateMessageCount();
|
||||
},
|
||||
updateSomeMessages: function() {
|
||||
this.benchmark("update every 10th", () => {
|
||||
const widgets = Object.values(this.widgets);
|
||||
for (let i = 0; i < widgets.length; i += 10) {
|
||||
widgets[i].update();
|
||||
}
|
||||
});
|
||||
},
|
||||
_onRemoveMessage: function(ev) {
|
||||
startMeasure("remove message");
|
||||
ev.target.destroy();
|
||||
delete this.widgets[ev.data.id];
|
||||
this.messageCount--;
|
||||
this.updateMessageCount();
|
||||
stopMeasure();
|
||||
},
|
||||
|
||||
benchmark: function(message, fn, callback) {
|
||||
if (this.multipleFlag) {
|
||||
const N = 20;
|
||||
let n = N;
|
||||
let total = 0;
|
||||
let cb = info => {
|
||||
let finalize = () => {
|
||||
n--;
|
||||
total += info.delta;
|
||||
if (n === 0) {
|
||||
const avg = total / N;
|
||||
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
this._benchmark(message, fn, cb);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.clearFlag) {
|
||||
this._benchmark("clear", this._clear.bind(this), finalize, false);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
this._benchmark(message, fn, cb);
|
||||
} else {
|
||||
this._benchmark(message, fn, callback);
|
||||
}
|
||||
},
|
||||
|
||||
_benchmark: function(message, fn, cb, log = true) {
|
||||
setTimeout(() => {
|
||||
startMeasure(message);
|
||||
const benchmark = fn();
|
||||
(benchmark && benchmark.then ? benchmark : $.when()).then(() => {
|
||||
stopMeasure(info => {
|
||||
if (log) {
|
||||
this.log(info.msg);
|
||||
}
|
||||
if (cb) {
|
||||
cb(info);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
});
|
||||
},
|
||||
|
||||
log: function(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.$log[0].appendChild(div);
|
||||
this.$log[0].scrollTop = this.$log[0].scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
// prepare QWeb
|
||||
const templates = await fetch("templates.xml");
|
||||
let strTemplates = await templates.text();
|
||||
strTemplates = strTemplates.replace(/<!--[\s\S]*?-->/g, "");
|
||||
core.qweb.add_template(strTemplates);
|
||||
|
||||
// prepare app
|
||||
const app = new App();
|
||||
await app.appendTo(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Odoo Widget Benchmark (12.3)</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='web.assets_common.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,47 @@
|
||||
<templates>
|
||||
<div class="main" t-name="root">
|
||||
<div class="left-thing">
|
||||
<div class="title">Actions</div>
|
||||
<div class="panel">
|
||||
<button class="o_btn_msg 100">Add 100 messages</button>
|
||||
<button class="o_btn_msg 1000">Add 1k messages</button>
|
||||
<button class="o_btn_msg 10000">Add 10k messages</button>
|
||||
<button class="o_btn_msg 30000">Add 30k messages</button>
|
||||
<button class="updateSomeMessages">Update every 10th message</button>
|
||||
<button class="clear">Clear</button>
|
||||
</div>
|
||||
<div class="flags">
|
||||
<div>
|
||||
<input type="checkbox" class="o_multiple" id="multipleflag" />
|
||||
<label for="multipleflag">Do it 20x</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" class="o_clear" id="clearFlag" />
|
||||
<label for="clearFlag">Clear after</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info message_count">
|
||||
Number of messages: 0
|
||||
</div>
|
||||
<div class="title">Log <span class="clear-log">(clear)</span></div>
|
||||
<div class="log">
|
||||
<div class="log-content"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="widget.author"/></span>
|
||||
<span class="msg"><t t-esc="widget.msg"/></span>
|
||||
<button class="remove">Remove</button>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button class="o_increment"></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,173 @@
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
state = { counter: 0 };
|
||||
template = "Counter";
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
widgets = { Counter };
|
||||
template = "Message";
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
widgets = { Message };
|
||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
template = "App";
|
||||
|
||||
mounted() {
|
||||
this.log(
|
||||
`Benchmarking Owl v${owl._version} (build date: ${
|
||||
owl._date
|
||||
})`
|
||||
);
|
||||
}
|
||||
|
||||
benchmark(message, fn, callback) {
|
||||
if (this.state.multipleFlag) {
|
||||
const N = 20;
|
||||
let n = N;
|
||||
let total = 0;
|
||||
let cb = info => {
|
||||
let finalize = () => {
|
||||
n--;
|
||||
total += info.delta;
|
||||
if (n === 0) {
|
||||
const avg = total / N;
|
||||
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
this._benchmark(message, fn, cb);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.state.clearAfterFlag) {
|
||||
this._benchmark(
|
||||
"clear",
|
||||
() => {
|
||||
this.state.messages = [];
|
||||
},
|
||||
finalize,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
this._benchmark(message, fn, cb);
|
||||
} else {
|
||||
this._benchmark(message, fn, callback);
|
||||
}
|
||||
}
|
||||
|
||||
_benchmark(message, fn, cb, log = true) {
|
||||
setTimeout(() => {
|
||||
startMeasure(message);
|
||||
fn();
|
||||
stopMeasure(info => {
|
||||
if (log) {
|
||||
this.log(info.msg);
|
||||
}
|
||||
if (cb) {
|
||||
cb(info);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
|
||||
addMessages(n) {
|
||||
this.benchmark("add " + n, () => {
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._benchmark("clear", () => {
|
||||
this.state.messages = [];
|
||||
});
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
this.benchmark("update every 10th", () => {
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += "!!!";
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
this.benchmark("remove message", () => {
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
log(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.refs.log.appendChild(div);
|
||||
this.refs.log.scrollTop = this.refs.log.scrollHeight;
|
||||
}
|
||||
|
||||
clearLog() {
|
||||
this.refs.log.innerHTML = "";
|
||||
}
|
||||
|
||||
toggleMultiple() {
|
||||
this.state.multipleFlag = !this.state.multipleFlag;
|
||||
}
|
||||
|
||||
toggleClear() {
|
||||
this.state.clearAfterFlag = !this.state.clearAfterFlag;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OWL 0.10.0 Benchmark</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='owl.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
<templates>
|
||||
<div t-name="App" class="main">
|
||||
<div class="left-thing">
|
||||
<div class="title">Actions</div>
|
||||
<div class="panel">
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1k messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10k messages</button>
|
||||
<button t-on-click="addMessages(30000)">Add 30k messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="flags">
|
||||
<div>
|
||||
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
|
||||
<label for="multipleflag">Do it 20x</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
|
||||
<label for="clearFlag">Clear after</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
|
||||
<hr/>
|
||||
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
|
||||
<div class="log">
|
||||
<div class="log-content" t-ref="'log'"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content" t-on-remove-message="removeMessage">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,170 @@
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
widgets = { Message };
|
||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
|
||||
mounted() {
|
||||
this.log(
|
||||
`Benchmarking Owl v${owl._version} (build date: ${
|
||||
owl._date
|
||||
})`
|
||||
);
|
||||
}
|
||||
|
||||
benchmark(message, fn, callback) {
|
||||
if (this.state.multipleFlag) {
|
||||
const N = 20;
|
||||
let n = N;
|
||||
let total = 0;
|
||||
let cb = info => {
|
||||
let finalize = () => {
|
||||
n--;
|
||||
total += info.delta;
|
||||
if (n === 0) {
|
||||
const avg = total / N;
|
||||
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
this._benchmark(message, fn, cb);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.state.clearAfterFlag) {
|
||||
this._benchmark(
|
||||
"clear",
|
||||
() => {
|
||||
this.state.messages = [];
|
||||
},
|
||||
finalize,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
this._benchmark(message, fn, cb);
|
||||
} else {
|
||||
this._benchmark(message, fn, callback);
|
||||
}
|
||||
}
|
||||
|
||||
_benchmark(message, fn, cb, log = true) {
|
||||
setTimeout(() => {
|
||||
startMeasure(message);
|
||||
fn();
|
||||
stopMeasure(info => {
|
||||
if (log) {
|
||||
this.log(info.msg);
|
||||
}
|
||||
if (cb) {
|
||||
cb(info);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
|
||||
addMessages(n) {
|
||||
this.benchmark("add " + n, () => {
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._benchmark("clear", () => {
|
||||
this.state.messages = [];
|
||||
});
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
this.benchmark("update every 10th", () => {
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += "!!!";
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
this.benchmark("remove message", () => {
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
log(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.refs.log.appendChild(div);
|
||||
this.refs.log.scrollTop = this.refs.log.scrollHeight;
|
||||
}
|
||||
|
||||
clearLog() {
|
||||
this.refs.log.innerHTML = "";
|
||||
}
|
||||
|
||||
toggleMultiple() {
|
||||
this.state.multipleFlag = !this.state.multipleFlag;
|
||||
}
|
||||
|
||||
toggleClear() {
|
||||
this.state.clearAfterFlag = !this.state.clearAfterFlag;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OWL 0.11.0 Benchmark</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='owl.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
<templates>
|
||||
<div t-name="App" class="main">
|
||||
<div class="left-thing">
|
||||
<div class="title">Actions</div>
|
||||
<div class="panel">
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1k messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10k messages</button>
|
||||
<button t-on-click="addMessages(30000)">Add 30k messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="flags">
|
||||
<div>
|
||||
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
|
||||
<label for="multipleflag">Do it 20x</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
|
||||
<label for="clearFlag">Clear after</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
|
||||
<hr/>
|
||||
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
|
||||
<div class="log">
|
||||
<div class="log-content" t-ref="'log'"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content" t-on-remove-message="removeMessage">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,170 @@
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
widgets = { Message };
|
||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
|
||||
mounted() {
|
||||
this.log(
|
||||
`Benchmarking Owl v${owl.__info__.version} (build date: ${
|
||||
owl.__info__.date
|
||||
})`
|
||||
);
|
||||
}
|
||||
|
||||
benchmark(message, fn, callback) {
|
||||
if (this.state.multipleFlag) {
|
||||
const N = 20;
|
||||
let n = N;
|
||||
let total = 0;
|
||||
let cb = info => {
|
||||
let finalize = () => {
|
||||
n--;
|
||||
total += info.delta;
|
||||
if (n === 0) {
|
||||
const avg = total / N;
|
||||
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
this._benchmark(message, fn, cb);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.state.clearAfterFlag) {
|
||||
this._benchmark(
|
||||
"clear",
|
||||
() => {
|
||||
this.state.messages = [];
|
||||
},
|
||||
finalize,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
this._benchmark(message, fn, cb);
|
||||
} else {
|
||||
this._benchmark(message, fn, callback);
|
||||
}
|
||||
}
|
||||
|
||||
_benchmark(message, fn, cb, log = true) {
|
||||
setTimeout(() => {
|
||||
startMeasure(message);
|
||||
fn();
|
||||
stopMeasure(info => {
|
||||
if (log) {
|
||||
this.log(info.msg);
|
||||
}
|
||||
if (cb) {
|
||||
cb(info);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
|
||||
addMessages(n) {
|
||||
this.benchmark("add " + n, () => {
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.benchmark("clear", () => {
|
||||
this.state.messages = [];
|
||||
});
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
this.benchmark("update every 10th", () => {
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += "!!!";
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
this.benchmark("remove message", () => {
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
log(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.refs.log.appendChild(div);
|
||||
this.refs.log.scrollTop = this.refs.log.scrollHeight;
|
||||
}
|
||||
|
||||
clearLog() {
|
||||
this.refs.log.innerHTML = "";
|
||||
}
|
||||
|
||||
toggleMultiple() {
|
||||
this.state.multipleFlag = !this.state.multipleFlag;
|
||||
}
|
||||
|
||||
toggleClear() {
|
||||
this.state.clearAfterFlag = !this.state.clearAfterFlag;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OWL 0.12.0 Benchmark</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='owl.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
<templates>
|
||||
<div t-name="App" class="main">
|
||||
<div class="left-thing">
|
||||
<div class="title">Actions</div>
|
||||
<div class="panel">
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1k messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10k messages</button>
|
||||
<button t-on-click="addMessages(30000)">Add 30k messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="flags">
|
||||
<div>
|
||||
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
|
||||
<label for="multipleflag">Do it 20x</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
|
||||
<label for="clearFlag">Clear after</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
|
||||
<hr/>
|
||||
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
|
||||
<div class="log">
|
||||
<div class="log-content" t-ref="log"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content" t-on-remove-message="removeMessage">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,161 @@
|
||||
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps.message !== this.props.message;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove-message", {
|
||||
id: this.props.message.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
widgets = { Message };
|
||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
|
||||
mounted() {
|
||||
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
|
||||
}
|
||||
|
||||
benchmark(message, fn, callback) {
|
||||
if (this.state.multipleFlag) {
|
||||
const N = 20;
|
||||
let n = N;
|
||||
let total = 0;
|
||||
let cb = info => {
|
||||
let finalize = () => {
|
||||
n--;
|
||||
total += info.delta;
|
||||
if (n === 0) {
|
||||
const avg = total / N;
|
||||
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
this._benchmark(message, fn, cb);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.state.clearAfterFlag) {
|
||||
this._benchmark(
|
||||
"clear",
|
||||
() => {
|
||||
this.state.messages = [];
|
||||
},
|
||||
finalize,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
this._benchmark(message, fn, cb);
|
||||
} else {
|
||||
this._benchmark(message, fn, callback);
|
||||
}
|
||||
}
|
||||
|
||||
_benchmark(message, fn, cb, log = true) {
|
||||
setTimeout(() => {
|
||||
startMeasure(message);
|
||||
fn();
|
||||
stopMeasure(info => {
|
||||
if (log) {
|
||||
this.log(info.msg);
|
||||
}
|
||||
if (cb) {
|
||||
cb(info);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
|
||||
addMessages(n) {
|
||||
this.benchmark("add " + n, () => {
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._benchmark("clear", () => {
|
||||
this.state.messages = [];
|
||||
});
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
this.benchmark("update every 10th", () => {
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += "!!!";
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeMessage(event) {
|
||||
this.benchmark("remove message", () => {
|
||||
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
log(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.refs.log.appendChild(div);
|
||||
this.refs.log.scrollTop = this.refs.log.scrollHeight;
|
||||
}
|
||||
|
||||
clearLog() {
|
||||
this.refs.log.innerHTML = "";
|
||||
}
|
||||
|
||||
toggleMultiple() {
|
||||
this.state.multipleFlag = !this.state.multipleFlag;
|
||||
}
|
||||
|
||||
toggleClear() {
|
||||
this.state.clearAfterFlag = !this.state.clearAfterFlag;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OWL 0.13.0 Benchmark</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='owl.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
<templates>
|
||||
<div t-name="App" class="main">
|
||||
<div class="left-thing">
|
||||
<div class="title">Actions</div>
|
||||
<div class="panel">
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1k messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10k messages</button>
|
||||
<button t-on-click="addMessages(30000)">Add 30k messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="flags">
|
||||
<div>
|
||||
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
|
||||
<label for="multipleflag">Do it 20x</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
|
||||
<label for="clearFlag">Clear after</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
|
||||
<hr/>
|
||||
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
|
||||
<div class="log">
|
||||
<div class="log-content" t-ref="log"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content" t-on-remove-message="removeMessage">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" message="message"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.message.author"/></span>
|
||||
<span class="msg"><t t-esc="props.message.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,161 @@
|
||||
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps.message !== this.props.message;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove-message", {
|
||||
id: this.props.message.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
widgets = { Message };
|
||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
|
||||
mounted() {
|
||||
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
|
||||
}
|
||||
|
||||
benchmark(message, fn, callback) {
|
||||
if (this.state.multipleFlag) {
|
||||
const N = 20;
|
||||
let n = N;
|
||||
let total = 0;
|
||||
let cb = info => {
|
||||
let finalize = () => {
|
||||
n--;
|
||||
total += info.delta;
|
||||
if (n === 0) {
|
||||
const avg = total / N;
|
||||
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
this._benchmark(message, fn, cb);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.state.clearAfterFlag) {
|
||||
this._benchmark(
|
||||
"clear",
|
||||
() => {
|
||||
this.state.messages = [];
|
||||
},
|
||||
finalize,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
this._benchmark(message, fn, cb);
|
||||
} else {
|
||||
this._benchmark(message, fn, callback);
|
||||
}
|
||||
}
|
||||
|
||||
_benchmark(message, fn, cb, log = true) {
|
||||
setTimeout(() => {
|
||||
startMeasure(message);
|
||||
fn();
|
||||
stopMeasure(info => {
|
||||
if (log) {
|
||||
this.log(info.msg);
|
||||
}
|
||||
if (cb) {
|
||||
cb(info);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
|
||||
addMessages(n) {
|
||||
this.benchmark("add " + n, () => {
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._benchmark("clear", () => {
|
||||
this.state.messages = [];
|
||||
});
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
this.benchmark("update every 10th", () => {
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += "!!!";
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeMessage(event) {
|
||||
this.benchmark("remove message", () => {
|
||||
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
log(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.refs.log.appendChild(div);
|
||||
this.refs.log.scrollTop = this.refs.log.scrollHeight;
|
||||
}
|
||||
|
||||
clearLog() {
|
||||
this.refs.log.innerHTML = "";
|
||||
}
|
||||
|
||||
toggleMultiple() {
|
||||
this.state.multipleFlag = !this.state.multipleFlag;
|
||||
}
|
||||
|
||||
toggleClear() {
|
||||
this.state.clearAfterFlag = !this.state.clearAfterFlag;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OWL 0.14.0 Benchmark</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='owl.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
<templates>
|
||||
<div t-name="App" class="main">
|
||||
<div class="left-thing">
|
||||
<div class="title">Actions</div>
|
||||
<div class="panel">
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1k messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10k messages</button>
|
||||
<button t-on-click="addMessages(30000)">Add 30k messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="flags">
|
||||
<div>
|
||||
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
|
||||
<label for="multipleflag">Do it 20x</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
|
||||
<label for="clearFlag">Clear after</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
|
||||
<hr/>
|
||||
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
|
||||
<div class="log">
|
||||
<div class="log-content" t-ref="log"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content" t-on-remove-message="removeMessage">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" message="message"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.message.author"/></span>
|
||||
<span class="msg"><t t-esc="props.message.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,161 @@
|
||||
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps.message !== this.props.message;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove-message", {
|
||||
id: this.props.message.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
widgets = { Message };
|
||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
|
||||
mounted() {
|
||||
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
|
||||
}
|
||||
|
||||
benchmark(message, fn, callback) {
|
||||
if (this.state.multipleFlag) {
|
||||
const N = 20;
|
||||
let n = N;
|
||||
let total = 0;
|
||||
let cb = info => {
|
||||
let finalize = () => {
|
||||
n--;
|
||||
total += info.delta;
|
||||
if (n === 0) {
|
||||
const avg = total / N;
|
||||
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
this._benchmark(message, fn, cb);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.state.clearAfterFlag) {
|
||||
this._benchmark(
|
||||
"clear",
|
||||
() => {
|
||||
this.state.messages = [];
|
||||
},
|
||||
finalize,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
this._benchmark(message, fn, cb);
|
||||
} else {
|
||||
this._benchmark(message, fn, callback);
|
||||
}
|
||||
}
|
||||
|
||||
_benchmark(message, fn, cb, log = true) {
|
||||
setTimeout(() => {
|
||||
startMeasure(message);
|
||||
fn();
|
||||
stopMeasure(info => {
|
||||
if (log) {
|
||||
this.log(info.msg);
|
||||
}
|
||||
if (cb) {
|
||||
cb(info);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
|
||||
addMessages(n) {
|
||||
this.benchmark("add " + n, () => {
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._benchmark("clear", () => {
|
||||
this.state.messages = [];
|
||||
});
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
this.benchmark("update every 10th", () => {
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += "!!!";
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeMessage(event) {
|
||||
this.benchmark("remove message", () => {
|
||||
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
log(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.refs.log.appendChild(div);
|
||||
this.refs.log.scrollTop = this.refs.log.scrollHeight;
|
||||
}
|
||||
|
||||
clearLog() {
|
||||
this.refs.log.innerHTML = "";
|
||||
}
|
||||
|
||||
toggleMultiple() {
|
||||
this.state.multipleFlag = !this.state.multipleFlag;
|
||||
}
|
||||
|
||||
toggleClear() {
|
||||
this.state.clearAfterFlag = !this.state.clearAfterFlag;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OWL 0.15.0 Benchmark</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='owl.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
<templates>
|
||||
<div t-name="App" class="main">
|
||||
<div class="left-thing">
|
||||
<div class="title">Actions</div>
|
||||
<div class="panel">
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1k messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10k messages</button>
|
||||
<button t-on-click="addMessages(30000)">Add 30k messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="flags">
|
||||
<div>
|
||||
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
|
||||
<label for="multipleflag">Do it 20x</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
|
||||
<label for="clearFlag">Clear after</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
|
||||
<hr/>
|
||||
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
|
||||
<div class="log">
|
||||
<div class="log-content" t-ref="log"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content" t-on-remove-message="removeMessage">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" message="message"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.message.author"/></span>
|
||||
<span class="msg"><t t-esc="props.message.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,173 @@
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
state = { counter: 0 };
|
||||
template = "Counter";
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
widgets = { Counter };
|
||||
template = "Message";
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
widgets = { Message };
|
||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
template = "App";
|
||||
|
||||
mounted() {
|
||||
this.log(
|
||||
`Benchmarking Owl v${owl._version} (build date: ${
|
||||
owl._date
|
||||
})`
|
||||
);
|
||||
}
|
||||
|
||||
benchmark(message, fn, callback) {
|
||||
if (this.state.multipleFlag) {
|
||||
const N = 20;
|
||||
let n = N;
|
||||
let total = 0;
|
||||
let cb = info => {
|
||||
let finalize = () => {
|
||||
n--;
|
||||
total += info.delta;
|
||||
if (n === 0) {
|
||||
const avg = total / N;
|
||||
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
this._benchmark(message, fn, cb);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.state.clearAfterFlag) {
|
||||
this._benchmark(
|
||||
"clear",
|
||||
() => {
|
||||
this.state.messages = [];
|
||||
},
|
||||
finalize,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
this._benchmark(message, fn, cb);
|
||||
} else {
|
||||
this._benchmark(message, fn, callback);
|
||||
}
|
||||
}
|
||||
|
||||
_benchmark(message, fn, cb, log = true) {
|
||||
setTimeout(() => {
|
||||
startMeasure(message);
|
||||
fn();
|
||||
stopMeasure(info => {
|
||||
if (log) {
|
||||
this.log(info.msg);
|
||||
}
|
||||
if (cb) {
|
||||
cb(info);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
|
||||
addMessages(n) {
|
||||
this.benchmark("add " + n, () => {
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._benchmark("clear", () => {
|
||||
this.state.messages = [];
|
||||
});
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
this.benchmark("update every 10th", () => {
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += "!!!";
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
this.benchmark("remove message", () => {
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
log(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.refs.log.appendChild(div);
|
||||
this.refs.log.scrollTop = this.refs.log.scrollHeight;
|
||||
}
|
||||
|
||||
clearLog() {
|
||||
this.refs.log.innerHTML = "";
|
||||
}
|
||||
|
||||
toggleMultiple() {
|
||||
this.state.multipleFlag = !this.state.multipleFlag;
|
||||
}
|
||||
|
||||
toggleClear() {
|
||||
this.state.clearAfterFlag = !this.state.clearAfterFlag;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OWL 0.7.0 Benchmark</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='owl.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
<templates>
|
||||
<div t-name="App" class="main">
|
||||
<div class="left-thing">
|
||||
<div class="title">Actions</div>
|
||||
<div class="panel">
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1k messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10k messages</button>
|
||||
<button t-on-click="addMessages(30000)">Add 30k messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="flags">
|
||||
<div>
|
||||
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
|
||||
<label for="multipleflag">Do it 20x</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
|
||||
<label for="clearFlag">Clear after</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
|
||||
<hr/>
|
||||
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
|
||||
<div class="log">
|
||||
<div class="log-content" t-ref="'log'"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content" t-on-remove-message="removeMessage">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,173 @@
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
state = { counter: 0 };
|
||||
template = "Counter";
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
widgets = { Counter };
|
||||
template = "Message";
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
widgets = { Message };
|
||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
template = "App";
|
||||
|
||||
mounted() {
|
||||
this.log(
|
||||
`Benchmarking Owl v${owl._version} (build date: ${
|
||||
owl._date
|
||||
})`
|
||||
);
|
||||
}
|
||||
|
||||
benchmark(message, fn, callback) {
|
||||
if (this.state.multipleFlag) {
|
||||
const N = 20;
|
||||
let n = N;
|
||||
let total = 0;
|
||||
let cb = info => {
|
||||
let finalize = () => {
|
||||
n--;
|
||||
total += info.delta;
|
||||
if (n === 0) {
|
||||
const avg = total / N;
|
||||
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
this._benchmark(message, fn, cb);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.state.clearAfterFlag) {
|
||||
this._benchmark(
|
||||
"clear",
|
||||
() => {
|
||||
this.state.messages = [];
|
||||
},
|
||||
finalize,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
this._benchmark(message, fn, cb);
|
||||
} else {
|
||||
this._benchmark(message, fn, callback);
|
||||
}
|
||||
}
|
||||
|
||||
_benchmark(message, fn, cb, log = true) {
|
||||
setTimeout(() => {
|
||||
startMeasure(message);
|
||||
fn();
|
||||
stopMeasure(info => {
|
||||
if (log) {
|
||||
this.log(info.msg);
|
||||
}
|
||||
if (cb) {
|
||||
cb(info);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
|
||||
addMessages(n) {
|
||||
this.benchmark("add " + n, () => {
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._benchmark("clear", () => {
|
||||
this.state.messages = [];
|
||||
});
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
this.benchmark("update every 10th", () => {
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += "!!!";
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
this.benchmark("remove message", () => {
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
log(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.refs.log.appendChild(div);
|
||||
this.refs.log.scrollTop = this.refs.log.scrollHeight;
|
||||
}
|
||||
|
||||
clearLog() {
|
||||
this.refs.log.innerHTML = "";
|
||||
}
|
||||
|
||||
toggleMultiple() {
|
||||
this.state.multipleFlag = !this.state.multipleFlag;
|
||||
}
|
||||
|
||||
toggleClear() {
|
||||
this.state.clearAfterFlag = !this.state.clearAfterFlag;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OWL 0.8.0 Benchmark</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='owl-0.8.0.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
<templates>
|
||||
<div t-name="App" class="main">
|
||||
<div class="left-thing">
|
||||
<div class="title">Actions</div>
|
||||
<div class="panel">
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1k messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10k messages</button>
|
||||
<button t-on-click="addMessages(30000)">Add 30k messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="flags">
|
||||
<div>
|
||||
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
|
||||
<label for="multipleflag">Do it 20x</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
|
||||
<label for="clearFlag">Clear after</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
|
||||
<hr/>
|
||||
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
|
||||
<div class="log">
|
||||
<div class="log-content" t-ref="'log'"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content" t-on-remove-message="removeMessage">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,173 @@
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
state = { counter: 0 };
|
||||
template = "Counter";
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
widgets = { Counter };
|
||||
template = "Message";
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
widgets = { Message };
|
||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
template = "App";
|
||||
|
||||
mounted() {
|
||||
this.log(
|
||||
`Benchmarking Owl v${owl._version} (build date: ${
|
||||
owl._date
|
||||
})`
|
||||
);
|
||||
}
|
||||
|
||||
benchmark(message, fn, callback) {
|
||||
if (this.state.multipleFlag) {
|
||||
const N = 20;
|
||||
let n = N;
|
||||
let total = 0;
|
||||
let cb = info => {
|
||||
let finalize = () => {
|
||||
n--;
|
||||
total += info.delta;
|
||||
if (n === 0) {
|
||||
const avg = total / N;
|
||||
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
this._benchmark(message, fn, cb);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.state.clearAfterFlag) {
|
||||
this._benchmark(
|
||||
"clear",
|
||||
() => {
|
||||
this.state.messages = [];
|
||||
},
|
||||
finalize,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
this._benchmark(message, fn, cb);
|
||||
} else {
|
||||
this._benchmark(message, fn, callback);
|
||||
}
|
||||
}
|
||||
|
||||
_benchmark(message, fn, cb, log = true) {
|
||||
setTimeout(() => {
|
||||
startMeasure(message);
|
||||
fn();
|
||||
stopMeasure(info => {
|
||||
if (log) {
|
||||
this.log(info.msg);
|
||||
}
|
||||
if (cb) {
|
||||
cb(info);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
|
||||
addMessages(n) {
|
||||
this.benchmark("add " + n, () => {
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._benchmark("clear", () => {
|
||||
this.state.messages = [];
|
||||
});
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
this.benchmark("update every 10th", () => {
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += "!!!";
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
this.benchmark("remove message", () => {
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
log(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.refs.log.appendChild(div);
|
||||
this.refs.log.scrollTop = this.refs.log.scrollHeight;
|
||||
}
|
||||
|
||||
clearLog() {
|
||||
this.refs.log.innerHTML = "";
|
||||
}
|
||||
|
||||
toggleMultiple() {
|
||||
this.state.multipleFlag = !this.state.multipleFlag;
|
||||
}
|
||||
|
||||
toggleClear() {
|
||||
this.state.clearAfterFlag = !this.state.clearAfterFlag;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OWL 0.9.0 Benchmark</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='owl.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
<templates>
|
||||
<div t-name="App" class="main">
|
||||
<div class="left-thing">
|
||||
<div class="title">Actions</div>
|
||||
<div class="panel">
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1k messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10k messages</button>
|
||||
<button t-on-click="addMessages(30000)">Add 30k messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="flags">
|
||||
<div>
|
||||
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
|
||||
<label for="multipleflag">Do it 20x</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
|
||||
<label for="clearFlag">Clear after</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
|
||||
<hr/>
|
||||
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
|
||||
<div class="log">
|
||||
<div class="log-content" t-ref="'log'"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content" t-on-remove-message="removeMessage">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,154 @@
|
||||
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
components = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps.message !== this.props.message;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove-message", {
|
||||
id: this.props.message.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
components = { Message };
|
||||
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
|
||||
mounted() {
|
||||
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
|
||||
}
|
||||
|
||||
benchmark(message, fn, callback) {
|
||||
if (this.state.multipleFlag) {
|
||||
const N = 20;
|
||||
let n = N;
|
||||
let total = 0;
|
||||
let cb = info => {
|
||||
let finalize = () => {
|
||||
n--;
|
||||
total += info.delta;
|
||||
if (n === 0) {
|
||||
const avg = total / N;
|
||||
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
this._benchmark(message, fn, cb);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.state.clearAfterFlag) {
|
||||
this._benchmark(
|
||||
"clear",
|
||||
() => {
|
||||
this.state.messages = [];
|
||||
},
|
||||
finalize,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
this._benchmark(message, fn, cb);
|
||||
} else {
|
||||
this._benchmark(message, fn, callback);
|
||||
}
|
||||
}
|
||||
|
||||
_benchmark(message, fn, cb, log = true) {
|
||||
setTimeout(() => {
|
||||
startMeasure(message);
|
||||
fn();
|
||||
stopMeasure(info => {
|
||||
if (log) {
|
||||
this.log(info.msg);
|
||||
}
|
||||
if (cb) {
|
||||
cb(info);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
|
||||
addMessages(n) {
|
||||
this.benchmark("add " + n, () => {
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._benchmark("clear", () => {
|
||||
this.state.messages = [];
|
||||
});
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
this.benchmark("update every 10th", () => {
|
||||
const setState = owl.Observer.set;
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += "!!!";
|
||||
setState(messages, i, msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeMessage(event) {
|
||||
this.benchmark("remove message", () => {
|
||||
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
log(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.refs.log.appendChild(div);
|
||||
this.refs.log.scrollTop = this.refs.log.scrollHeight;
|
||||
}
|
||||
|
||||
clearLog() {
|
||||
this.refs.log.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OWL Master Benchmark</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='../../owl.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,50 @@
|
||||
<templates>
|
||||
<div t-name="App" class="main">
|
||||
<div class="left-thing">
|
||||
<div class="title">Actions</div>
|
||||
<div class="panel">
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1k messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10k messages</button>
|
||||
<button t-on-click="addMessages(30000)">Add 30k messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="flags">
|
||||
<div>
|
||||
<input type="checkbox" id="multipleflag" t-model="multipleFlag"/>
|
||||
<label for="multipleflag">Do it 20x</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="clearFlag" t-model="clearAfterFlag" />
|
||||
<label for="clearFlag">Clear after</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
|
||||
<hr/>
|
||||
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
|
||||
<div class="log">
|
||||
<div class="log-content" t-ref="log"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content" t-on-remove-message="removeMessage">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<Message t-key="message.id" message="message"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="Message" class="message">
|
||||
<span class="author"><t t-esc="props.message.author"/></span>
|
||||
<span class="msg"><t t-esc="props.message.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<Counter/>
|
||||
</div>
|
||||
|
||||
<div t-name="Counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,346 @@
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
counter: 0
|
||||
};
|
||||
this.increment = this.increment.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return React.createElement(
|
||||
"div",
|
||||
null,
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: this.increment
|
||||
},
|
||||
"Value: ",
|
||||
this.state.counter
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.setState({
|
||||
counter: this.state.counter + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.removeMessage = this.removeMessage.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "message"
|
||||
},
|
||||
React.createElement(
|
||||
"span",
|
||||
{
|
||||
className: "author"
|
||||
},
|
||||
this.props.message.author
|
||||
),
|
||||
React.createElement(
|
||||
"span",
|
||||
{
|
||||
className: "msg"
|
||||
},
|
||||
this.props.message.msg
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
className: "remove",
|
||||
onClick: this.removeMessage
|
||||
},
|
||||
"Remove"
|
||||
),
|
||||
React.createElement(Counter, null)
|
||||
);
|
||||
}
|
||||
|
||||
removeMessage() {
|
||||
this.props.removeCB(this.props.message.id);
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Main extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
messages: [],
|
||||
multipleFlag: false,
|
||||
clearAfterFlag: false
|
||||
};
|
||||
this.removeMessage = this.removeMessage.bind(this);
|
||||
this.toggleMultipleFlag = this.toggleMultipleFlag.bind(this);
|
||||
this.toggleClearAfterFlag = this.toggleClearAfterFlag.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.log(`Benchmarking React v${React.version}`);
|
||||
}
|
||||
log(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.refs.logref.appendChild(div);
|
||||
this.refs.logref.scrollTop = this.refs.logref.scrollHeight;
|
||||
}
|
||||
|
||||
toggleMultipleFlag() {
|
||||
this.setState({ multipleFlag: !this.state.multipleFlag });
|
||||
}
|
||||
toggleClearAfterFlag() {
|
||||
this.setState({ clearAfterFlag: !this.state.clearAfterFlag });
|
||||
}
|
||||
|
||||
render() {
|
||||
const msgList = this.state.messages.map(m =>
|
||||
React.createElement(Message, {
|
||||
key: m.id,
|
||||
message: m,
|
||||
removeCB: this.removeMessage
|
||||
})
|
||||
);
|
||||
return React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "main"
|
||||
},
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "left-thing"
|
||||
},
|
||||
React.createElement("div", { className: "title" }, "Actions"),
|
||||
React.createElement(
|
||||
"div",
|
||||
{ className: "panel" },
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(100)
|
||||
},
|
||||
"Add 100 messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(1000)
|
||||
},
|
||||
"Add 1k messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(10000)
|
||||
},
|
||||
"Add 10k messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(30000)
|
||||
},
|
||||
"Add 30k messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.updateSomeMessages()
|
||||
},
|
||||
"Update every 10th message"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.clear()
|
||||
},
|
||||
"Clear"
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
"div",
|
||||
{ className: "flags" },
|
||||
React.createElement(
|
||||
"div",
|
||||
null,
|
||||
React.createElement("input", {
|
||||
type: "checkbox",
|
||||
id: "multipleFlag",
|
||||
value: this.state.multipleFlag,
|
||||
onChange: this.toggleMultipleFlag
|
||||
}),
|
||||
React.createElement("label", { for: "multipleFlag" }, " Do it 20x")
|
||||
),
|
||||
React.createElement(
|
||||
"div",
|
||||
null,
|
||||
React.createElement("input", {
|
||||
type: "checkbox",
|
||||
id: "clearFlag",
|
||||
value: this.state.clearAfterFlag,
|
||||
onChange: this.toggleClearAfterFlag
|
||||
}),
|
||||
React.createElement("label", { for: "clearFlag" }, " Clear after")
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
"div",
|
||||
{ className: "info" },
|
||||
"Number of messages: ",
|
||||
this.state.messages.length
|
||||
),
|
||||
React.createElement("hr"),
|
||||
React.createElement(
|
||||
"div",
|
||||
{ className: "title" },
|
||||
"Log ",
|
||||
React.createElement("span", { className: "clear-log" }, "(clear)")
|
||||
),
|
||||
React.createElement(
|
||||
"div",
|
||||
{ className: "log" },
|
||||
React.createElement("div", {
|
||||
className: "log-content",
|
||||
ref: "logref"
|
||||
})
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "right-thing"
|
||||
},
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "content"
|
||||
},
|
||||
msgList
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
addMessages(n) {
|
||||
this.benchmark("add " + n, () => {
|
||||
const newMessages = this.state.messages.concat(buildData(n));
|
||||
this.setState({
|
||||
messages: newMessages
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._benchmark("clear", () => {
|
||||
this.setState({
|
||||
messages: []
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
this.benchmark("update every 10th", () => {
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < messages.length; i += 10) {
|
||||
messages[i].author += "!!!";
|
||||
}
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
removeMessage(id) {
|
||||
startMeasure("remove message");
|
||||
const index = this.state.messages.findIndex(m => m.id === id);
|
||||
const messages = this.state.messages.slice();
|
||||
messages.splice(index, 1);
|
||||
this.setState({
|
||||
messages
|
||||
});
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
benchmark(message, fn, callback) {
|
||||
if (this.state.multipleFlag) {
|
||||
const N = 20;
|
||||
let n = N;
|
||||
let total = 0;
|
||||
let cb = info => {
|
||||
let finalize = () => {
|
||||
n--;
|
||||
total += info.delta;
|
||||
if (n === 0) {
|
||||
const avg = total / N;
|
||||
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
this._benchmark(message, fn, cb);
|
||||
}
|
||||
};
|
||||
if (this.state.clearAfterFlag) {
|
||||
this._benchmark(
|
||||
"clear",
|
||||
() => {
|
||||
this.setState({ messages: [] });
|
||||
},
|
||||
finalize,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
this._benchmark(message, fn, cb);
|
||||
} else {
|
||||
this._benchmark(message, fn, callback);
|
||||
}
|
||||
}
|
||||
|
||||
_benchmark(message, fn, cb, log = true) {
|
||||
setTimeout(() => {
|
||||
startMeasure(message);
|
||||
fn();
|
||||
stopMeasure(info => {
|
||||
if (log) {
|
||||
this.log(info.msg);
|
||||
}
|
||||
if (cb) {
|
||||
cb(info);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
} //-----------------------------
|
||||
// INIT
|
||||
//-----------------------------
|
||||
|
||||
ReactDOM.render(React.createElement(Main, null), document.body);
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>React Benchmark</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='react.production.min.js'></script>
|
||||
<script src='react-dom.production.min.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
/** @license React v16.8.6
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
/*
|
||||
Modernizr 3.0.0pre (Custom Build) | MIT
|
||||
*/
|
||||
'use strict';(function(da,pb){"object"===typeof exports&&"undefined"!==typeof module?module.exports=pb(require("react")):"function"===typeof define&&define.amd?define(["react"],pb):da.ReactDOM=pb(da.React)})(this,function(da){function pb(a,b,c,d,e,f,g,h){if(!a){a=void 0;if(void 0===b)a=Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var l=[c,d,e,f,g,h],k=0;a=Error(b.replace(/%s/g,function(){return l[k++]}));
|
||||
a.name="Invariant Violation"}a.framesToPop=1;throw a;}}function n(a){for(var b=arguments.length-1,c="https://reactjs.org/docs/error-decoder.html?invariant="+a,d=0;d<b;d++)c+="&args[]="+encodeURIComponent(arguments[d+1]);pb(!1,"Minified React error #"+a+"; visit %s for the full message or use the non-minified dev environment for full errors and additional helpful warnings. ",c)}function rh(a,b,c,d,e,f,g,h,l){qb=!1;$b=null;sh.apply(th,arguments)}function uh(a,b,c,d,e,f,g,h,l){rh.apply(this,arguments);
|
||||
if(qb){if(qb){var k=$b;qb=!1;$b=null}else n("198"),k=void 0;ac||(ac=!0,Yc=k)}}function Ee(){if(bc)for(var a in Na){var b=Na[a],c=bc.indexOf(a);-1<c?void 0:n("96",a);if(!cc[c]){b.extractEvents?void 0:n("97",a);cc[c]=b;c=b.eventTypes;for(var d in c){var e=void 0;var f=c[d],g=b,h=d;Zc.hasOwnProperty(h)?n("99",h):void 0;Zc[h]=f;var l=f.phasedRegistrationNames;if(l){for(e in l)l.hasOwnProperty(e)&&Fe(l[e],g,h);e=!0}else f.registrationName?(Fe(f.registrationName,g,h),e=!0):e=!1;e?void 0:n("98",d,a)}}}}
|
||||
function Fe(a,b,c){Oa[a]?n("100",a):void 0;Oa[a]=b;$c[a]=b.eventTypes[c].dependencies}function Ge(a,b,c){var d=a.type||"unknown-event";a.currentTarget=He(c);uh(d,b,void 0,a);a.currentTarget=null}function Pa(a,b){null==b?n("30"):void 0;if(null==a)return b;if(Array.isArray(a)){if(Array.isArray(b))return a.push.apply(a,b),a;a.push(b);return a}return Array.isArray(b)?[a].concat(b):[a,b]}function ad(a,b,c){Array.isArray(a)?a.forEach(b,c):a&&b.call(c,a)}function Ie(a,b){var c=a.stateNode;if(!c)return null;
|
||||
var d=bd(c);if(!d)return null;c=d[b];a:switch(b){case "onClick":case "onClickCapture":case "onDoubleClick":case "onDoubleClickCapture":case "onMouseDown":case "onMouseDownCapture":case "onMouseMove":case "onMouseMoveCapture":case "onMouseUp":case "onMouseUpCapture":(d=!d.disabled)||(a=a.type,d=!("button"===a||"input"===a||"select"===a||"textarea"===a));a=!d;break a;default:a=!1}if(a)return null;c&&"function"!==typeof c?n("231",b,typeof c):void 0;return c}function cd(a){null!==a&&(rb=Pa(rb,a));a=rb;
|
||||
rb=null;if(a&&(ad(a,vh),rb?n("95"):void 0,ac))throw a=Yc,ac=!1,Yc=null,a;}function dc(a){if(a[ea])return a[ea];for(;!a[ea];)if(a.parentNode)a=a.parentNode;else return null;a=a[ea];return 5===a.tag||6===a.tag?a:null}function Je(a){a=a[ea];return!a||5!==a.tag&&6!==a.tag?null:a}function Da(a){if(5===a.tag||6===a.tag)return a.stateNode;n("33")}function dd(a){return a[ec]||null}function fa(a){do a=a.return;while(a&&5!==a.tag);return a?a:null}function Ke(a,b,c){if(b=Ie(a,c.dispatchConfig.phasedRegistrationNames[b]))c._dispatchListeners=
|
||||
Pa(c._dispatchListeners,b),c._dispatchInstances=Pa(c._dispatchInstances,a)}function wh(a){if(a&&a.dispatchConfig.phasedRegistrationNames){for(var b=a._targetInst,c=[];b;)c.push(b),b=fa(b);for(b=c.length;0<b--;)Ke(c[b],"captured",a);for(b=0;b<c.length;b++)Ke(c[b],"bubbled",a)}}function ed(a,b,c){a&&c&&c.dispatchConfig.registrationName&&(b=Ie(a,c.dispatchConfig.registrationName))&&(c._dispatchListeners=Pa(c._dispatchListeners,b),c._dispatchInstances=Pa(c._dispatchInstances,a))}function xh(a){a&&a.dispatchConfig.registrationName&&
|
||||
ed(a._targetInst,null,a)}function Qa(a){ad(a,wh)}function fc(a,b){var c={};c[a.toLowerCase()]=b.toLowerCase();c["Webkit"+a]="webkit"+b;c["Moz"+a]="moz"+b;return c}function gc(a){if(fd[a])return fd[a];if(!Ra[a])return a;var b=Ra[a],c;for(c in b)if(b.hasOwnProperty(c)&&c in Le)return fd[a]=b[c];return a}function Me(){if(hc)return hc;var a,b=gd,c=b.length,d,e="value"in qa?qa.value:qa.textContent,f=e.length;for(a=0;a<c&&b[a]===e[a];a++);var g=c-a;for(d=1;d<=g&&b[c-d]===e[f-d];d++);return hc=e.slice(a,
|
||||
1<d?1-d:void 0)}function ic(){return!0}function jc(){return!1}function J(a,b,c,d){this.dispatchConfig=a;this._targetInst=b;this.nativeEvent=c;a=this.constructor.Interface;for(var e in a)a.hasOwnProperty(e)&&((b=a[e])?this[e]=b(c):"target"===e?this.target=d:this[e]=c[e]);this.isDefaultPrevented=(null!=c.defaultPrevented?c.defaultPrevented:!1===c.returnValue)?ic:jc;this.isPropagationStopped=jc;return this}function yh(a,b,c,d){if(this.eventPool.length){var e=this.eventPool.pop();this.call(e,a,b,c,d);
|
||||
return e}return new this(a,b,c,d)}function zh(a){a instanceof this?void 0:n("279");a.destructor();10>this.eventPool.length&&this.eventPool.push(a)}function Ne(a){a.eventPool=[];a.getPooled=yh;a.release=zh}function Oe(a,b){switch(a){case "keyup":return-1!==Ah.indexOf(b.keyCode);case "keydown":return 229!==b.keyCode;case "keypress":case "mousedown":case "blur":return!0;default:return!1}}function Pe(a){a=a.detail;return"object"===typeof a&&"data"in a?a.data:null}function Bh(a,b){switch(a){case "compositionend":return Pe(b);
|
||||
case "keypress":if(32!==b.which)return null;Qe=!0;return Re;case "textInput":return a=b.data,a===Re&&Qe?null:a;default:return null}}function Ch(a,b){if(Sa)return"compositionend"===a||!hd&&Oe(a,b)?(a=Me(),hc=gd=qa=null,Sa=!1,a):null;switch(a){case "paste":return null;case "keypress":if(!(b.ctrlKey||b.altKey||b.metaKey)||b.ctrlKey&&b.altKey){if(b.char&&1<b.char.length)return b.char;if(b.which)return String.fromCharCode(b.which)}return null;case "compositionend":return Se&&"ko"!==b.locale?null:b.data;
|
||||
default:return null}}function Te(a){if(a=Ue(a)){"function"!==typeof id?n("280"):void 0;var b=bd(a.stateNode);id(a.stateNode,a.type,b)}}function Ve(a){Ta?Ua?Ua.push(a):Ua=[a]:Ta=a}function We(){if(Ta){var a=Ta,b=Ua;Ua=Ta=null;Te(a);if(b)for(a=0;a<b.length;a++)Te(b[a])}}function Xe(a,b){if(jd)return a(b);jd=!0;try{return Ye(a,b)}finally{if(jd=!1,null!==Ta||null!==Ua)Ze(),We()}}function $e(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return"input"===b?!!Dh[a.type]:"textarea"===b?!0:!1}function kd(a){a=
|
||||
a.target||a.srcElement||window;a.correspondingUseElement&&(a=a.correspondingUseElement);return 3===a.nodeType?a.parentNode:a}function af(a){if(!ra)return!1;a="on"+a;var b=a in document;b||(b=document.createElement("div"),b.setAttribute(a,"return;"),b="function"===typeof b[a]);return b}function bf(a){var b=a.type;return(a=a.nodeName)&&"input"===a.toLowerCase()&&("checkbox"===b||"radio"===b)}function Eh(a){var b=bf(a)?"checked":"value",c=Object.getOwnPropertyDescriptor(a.constructor.prototype,b),d=
|
||||
""+a[b];if(!a.hasOwnProperty(b)&&"undefined"!==typeof c&&"function"===typeof c.get&&"function"===typeof c.set){var e=c.get,f=c.set;Object.defineProperty(a,b,{configurable:!0,get:function(){return e.call(this)},set:function(a){d=""+a;f.call(this,a)}});Object.defineProperty(a,b,{enumerable:c.enumerable});return{getValue:function(){return d},setValue:function(a){d=""+a},stopTracking:function(){a._valueTracker=null;delete a[b]}}}}function kc(a){a._valueTracker||(a._valueTracker=Eh(a))}function cf(a){if(!a)return!1;
|
||||
var b=a._valueTracker;if(!b)return!0;var c=b.getValue();var d="";a&&(d=bf(a)?a.checked?"true":"false":a.value);a=d;return a!==c?(b.setValue(a),!0):!1}function sb(a){if(null===a||"object"!==typeof a)return null;a=df&&a[df]||a["@@iterator"];return"function"===typeof a?a:null}function sa(a){if(null==a)return null;if("function"===typeof a)return a.displayName||a.name||null;if("string"===typeof a)return a;switch(a){case ld:return"ConcurrentMode";case ta:return"Fragment";case Va:return"Portal";case lc:return"Profiler";
|
||||
case md:return"StrictMode";case nd:return"Suspense"}if("object"===typeof a)switch(a.$$typeof){case ef:return"Context.Consumer";case ff:return"Context.Provider";case od:var b=a.render;b=b.displayName||b.name||"";return a.displayName||(""!==b?"ForwardRef("+b+")":"ForwardRef");case pd:return sa(a.type);case gf:if(a=1===a._status?a._result:null)return sa(a)}return null}function qd(a){var b="";do{a:switch(a.tag){case 3:case 4:case 6:case 7:case 10:case 9:var c="";break a;default:var d=a._debugOwner,e=
|
||||
a._debugSource,f=sa(a.type);c=null;d&&(c=sa(d.type));d=f;f="";e?f=" (at "+e.fileName.replace(Fh,"")+":"+e.lineNumber+")":c&&(f=" (created by "+c+")");c="\n in "+(d||"Unknown")+f}b+=c;a=a.return}while(a);return b}function Gh(a){if(hf.call(jf,a))return!0;if(hf.call(kf,a))return!1;if(Hh.test(a))return jf[a]=!0;kf[a]=!0;return!1}function Ih(a,b,c,d){if(null!==c&&0===c.type)return!1;switch(typeof b){case "function":case "symbol":return!0;case "boolean":if(d)return!1;if(null!==c)return!c.acceptsBooleans;
|
||||
a=a.toLowerCase().slice(0,5);return"data-"!==a&&"aria-"!==a;default:return!1}}function Jh(a,b,c,d){if(null===b||"undefined"===typeof b||Ih(a,b,c,d))return!0;if(d)return!1;if(null!==c)switch(c.type){case 3:return!b;case 4:return!1===b;case 5:return isNaN(b);case 6:return isNaN(b)||1>b}return!1}function K(a,b,c,d,e){this.acceptsBooleans=2===b||3===b||4===b;this.attributeName=d;this.attributeNamespace=e;this.mustUseProperty=c;this.propertyName=a;this.type=b}function rd(a,b,c,d){var e=A.hasOwnProperty(b)?
|
||||
A[b]:null;var f=null!==e?0===e.type:d?!1:!(2<b.length)||"o"!==b[0]&&"O"!==b[0]||"n"!==b[1]&&"N"!==b[1]?!1:!0;f||(Jh(b,c,e,d)&&(c=null),d||null===e?Gh(b)&&(null===c?a.removeAttribute(b):a.setAttribute(b,""+c)):e.mustUseProperty?a[e.propertyName]=null===c?3===e.type?!1:"":c:(b=e.attributeName,d=e.attributeNamespace,null===c?a.removeAttribute(b):(e=e.type,c=3===e||4===e&&!0===c?"":""+c,d?a.setAttributeNS(d,b,c):a.setAttribute(b,c))))}function ua(a){switch(typeof a){case "boolean":case "number":case "object":case "string":case "undefined":return a;
|
||||
default:return""}}function sd(a,b){var c=b.checked;return B({},b,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:null!=c?c:a._wrapperState.initialChecked})}function lf(a,b){var c=null==b.defaultValue?"":b.defaultValue,d=null!=b.checked?b.checked:b.defaultChecked;c=ua(null!=b.value?b.value:c);a._wrapperState={initialChecked:d,initialValue:c,controlled:"checkbox"===b.type||"radio"===b.type?null!=b.checked:null!=b.value}}function mf(a,b){b=b.checked;null!=b&&rd(a,"checked",b,!1)}function td(a,
|
||||
b){mf(a,b);var c=ua(b.value),d=b.type;if(null!=c)if("number"===d){if(0===c&&""===a.value||a.value!=c)a.value=""+c}else a.value!==""+c&&(a.value=""+c);else if("submit"===d||"reset"===d){a.removeAttribute("value");return}b.hasOwnProperty("value")?ud(a,b.type,c):b.hasOwnProperty("defaultValue")&&ud(a,b.type,ua(b.defaultValue));null==b.checked&&null!=b.defaultChecked&&(a.defaultChecked=!!b.defaultChecked)}function nf(a,b,c){if(b.hasOwnProperty("value")||b.hasOwnProperty("defaultValue")){var d=b.type;
|
||||
if(!("submit"!==d&&"reset"!==d||void 0!==b.value&&null!==b.value))return;b=""+a._wrapperState.initialValue;c||b===a.value||(a.value=b);a.defaultValue=b}c=a.name;""!==c&&(a.name="");a.defaultChecked=!a.defaultChecked;a.defaultChecked=!!a._wrapperState.initialChecked;""!==c&&(a.name=c)}function ud(a,b,c){if("number"!==b||a.ownerDocument.activeElement!==a)null==c?a.defaultValue=""+a._wrapperState.initialValue:a.defaultValue!==""+c&&(a.defaultValue=""+c)}function of(a,b,c){a=J.getPooled(pf.change,a,b,
|
||||
c);a.type="change";Ve(c);Qa(a);return a}function Kh(a){cd(a)}function mc(a){var b=Da(a);if(cf(b))return a}function Lh(a,b){if("change"===a)return b}function qf(){tb&&(tb.detachEvent("onpropertychange",rf),ub=tb=null)}function rf(a){"value"===a.propertyName&&mc(ub)&&(a=of(ub,a,kd(a)),Xe(Kh,a))}function Mh(a,b,c){"focus"===a?(qf(),tb=b,ub=c,tb.attachEvent("onpropertychange",rf)):"blur"===a&&qf()}function Nh(a,b){if("selectionchange"===a||"keyup"===a||"keydown"===a)return mc(ub)}function Oh(a,b){if("click"===
|
||||
a)return mc(b)}function Ph(a,b){if("input"===a||"change"===a)return mc(b)}function Qh(a){var b=this.nativeEvent;return b.getModifierState?b.getModifierState(a):(a=Rh[a])?!!b[a]:!1}function vd(a){return Qh}function Ea(a,b){return a===b&&(0!==a||1/a===1/b)||a!==a&&b!==b}function vb(a,b){if(Ea(a,b))return!0;if("object"!==typeof a||null===a||"object"!==typeof b||null===b)return!1;var c=Object.keys(a),d=Object.keys(b);if(c.length!==d.length)return!1;for(d=0;d<c.length;d++)if(!Sh.call(b,c[d])||!Ea(a[c[d]],
|
||||
b[c[d]]))return!1;return!0}function wb(a){var b=a;if(a.alternate)for(;b.return;)b=b.return;else{if(0!==(b.effectTag&2))return 1;for(;b.return;)if(b=b.return,0!==(b.effectTag&2))return 1}return 3===b.tag?2:3}function sf(a){2!==wb(a)?n("188"):void 0}function Th(a){var b=a.alternate;if(!b)return b=wb(a),3===b?n("188"):void 0,1===b?null:a;for(var c=a,d=b;;){var e=c.return,f=e?e.alternate:null;if(!e||!f)break;if(e.child===f.child){for(var g=e.child;g;){if(g===c)return sf(e),a;if(g===d)return sf(e),b;g=
|
||||
g.sibling}n("188")}if(c.return!==d.return)c=e,d=f;else{g=!1;for(var h=e.child;h;){if(h===c){g=!0;c=e;d=f;break}if(h===d){g=!0;d=e;c=f;break}h=h.sibling}if(!g){for(h=f.child;h;){if(h===c){g=!0;c=f;d=e;break}if(h===d){g=!0;d=f;c=e;break}h=h.sibling}g?void 0:n("189")}}c.alternate!==d?n("190"):void 0}3!==c.tag?n("188"):void 0;return c.stateNode.current===c?a:b}function tf(a){a=Th(a);if(!a)return null;for(var b=a;;){if(5===b.tag||6===b.tag)return b;if(b.child)b.child.return=b,b=b.child;else{if(b===a)break;
|
||||
for(;!b.sibling;){if(!b.return||b.return===a)return null;b=b.return}b.sibling.return=b.return;b=b.sibling}}return null}function nc(a){var b=a.keyCode;"charCode"in a?(a=a.charCode,0===a&&13===b&&(a=13)):a=b;10===a&&(a=13);return 32<=a||13===a?a:0}function uf(a,b){var c=a[0];a=a[1];var d="on"+(a[0].toUpperCase()+a.slice(1));b={phasedRegistrationNames:{bubbled:d,captured:d+"Capture"},dependencies:[c],isInteractive:b};vf[a]=b;wd[c]=b}function Uh(a){var b=a.targetInst,c=b;do{if(!c){a.ancestors.push(c);
|
||||
break}var d;for(d=c;d.return;)d=d.return;d=3!==d.tag?null:d.stateNode.containerInfo;if(!d)break;a.ancestors.push(c);c=dc(d)}while(c);for(c=0;c<a.ancestors.length;c++){b=a.ancestors[c];var e=kd(a.nativeEvent);d=a.topLevelType;for(var f=a.nativeEvent,g=null,h=0;h<cc.length;h++){var l=cc[h];l&&(l=l.extractEvents(d,b,f,e))&&(g=Pa(g,l))}cd(g)}}function r(a,b){if(!b)return null;var c=(wf(a)?xf:oc).bind(null,a);b.addEventListener(a,c,!1)}function pc(a,b){if(!b)return null;var c=(wf(a)?xf:oc).bind(null,a);
|
||||
b.addEventListener(a,c,!0)}function xf(a,b){yf(oc,a,b)}function oc(a,b){if(qc){var c=kd(b);c=dc(c);null===c||"number"!==typeof c.tag||2===wb(c)||(c=null);if(rc.length){var d=rc.pop();d.topLevelType=a;d.nativeEvent=b;d.targetInst=c;a=d}else a={topLevelType:a,nativeEvent:b,targetInst:c,ancestors:[]};try{Xe(Uh,a)}finally{a.topLevelType=null,a.nativeEvent=null,a.targetInst=null,a.ancestors.length=0,10>rc.length&&rc.push(a)}}}function zf(a){Object.prototype.hasOwnProperty.call(a,sc)||(a[sc]=Vh++,Af[a[sc]]=
|
||||
{});return Af[a[sc]]}function xd(a){a=a||("undefined"!==typeof document?document:void 0);if("undefined"===typeof a)return null;try{return a.activeElement||a.body}catch(b){return a.body}}function Bf(a){for(;a&&a.firstChild;)a=a.firstChild;return a}function Cf(a,b){var c=Bf(a);a=0;for(var d;c;){if(3===c.nodeType){d=a+c.textContent.length;if(a<=b&&d>=b)return{node:c,offset:b-a};a=d}a:{for(;c;){if(c.nextSibling){c=c.nextSibling;break a}c=c.parentNode}c=void 0}c=Bf(c)}}function Df(a,b){return a&&b?a===
|
||||
b?!0:a&&3===a.nodeType?!1:b&&3===b.nodeType?Df(a,b.parentNode):"contains"in a?a.contains(b):a.compareDocumentPosition?!!(a.compareDocumentPosition(b)&16):!1:!1}function Ef(){for(var a=window,b=xd();b instanceof a.HTMLIFrameElement;){try{var c="string"===typeof b.contentWindow.location.href}catch(d){c=!1}if(c)a=b.contentWindow;else break;b=xd(a.document)}return b}function yd(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return b&&("input"===b&&("text"===a.type||"search"===a.type||"tel"===a.type||
|
||||
"url"===a.type||"password"===a.type)||"textarea"===b||"true"===a.contentEditable)}function Wh(){var a=Ef();if(yd(a)){if("selectionStart"in a)var b={start:a.selectionStart,end:a.selectionEnd};else a:{b=(b=a.ownerDocument)&&b.defaultView||window;var c=b.getSelection&&b.getSelection();if(c&&0!==c.rangeCount){b=c.anchorNode;var d=c.anchorOffset,e=c.focusNode;c=c.focusOffset;try{b.nodeType,e.nodeType}catch(cj){b=null;break a}var f=0,g=-1,h=-1,l=0,k=0,m=a,n=null;b:for(;;){for(var p;;){m!==b||0!==d&&3!==
|
||||
m.nodeType||(g=f+d);m!==e||0!==c&&3!==m.nodeType||(h=f+c);3===m.nodeType&&(f+=m.nodeValue.length);if(null===(p=m.firstChild))break;n=m;m=p}for(;;){if(m===a)break b;n===b&&++l===d&&(g=f);n===e&&++k===c&&(h=f);if(null!==(p=m.nextSibling))break;m=n;n=m.parentNode}m=p}b=-1===g||-1===h?null:{start:g,end:h}}else b=null}b=b||{start:0,end:0}}else b=null;return{focusedElem:a,selectionRange:b}}function Xh(a){var b=Ef(),c=a.focusedElem,d=a.selectionRange;if(b!==c&&c&&c.ownerDocument&&Df(c.ownerDocument.documentElement,
|
||||
c)){if(null!==d&&yd(c))if(b=d.start,a=d.end,void 0===a&&(a=b),"selectionStart"in c)c.selectionStart=b,c.selectionEnd=Math.min(a,c.value.length);else if(a=(b=c.ownerDocument||document)&&b.defaultView||window,a.getSelection){a=a.getSelection();var e=c.textContent.length,f=Math.min(d.start,e);d=void 0===d.end?f:Math.min(d.end,e);!a.extend&&f>d&&(e=d,d=f,f=e);e=Cf(c,f);var g=Cf(c,d);e&&g&&(1!==a.rangeCount||a.anchorNode!==e.node||a.anchorOffset!==e.offset||a.focusNode!==g.node||a.focusOffset!==g.offset)&&
|
||||
(b=b.createRange(),b.setStart(e.node,e.offset),a.removeAllRanges(),f>d?(a.addRange(b),a.extend(g.node,g.offset)):(b.setEnd(g.node,g.offset),a.addRange(b)))}b=[];for(a=c;a=a.parentNode;)1===a.nodeType&&b.push({element:a,left:a.scrollLeft,top:a.scrollTop});"function"===typeof c.focus&&c.focus();for(c=0;c<b.length;c++)a=b[c],a.element.scrollLeft=a.left,a.element.scrollTop=a.top}}function Gf(a,b){var c=b.window===b?b.document:9===b.nodeType?b:b.ownerDocument;if(zd||null==Wa||Wa!==xd(c))return null;c=
|
||||
Wa;"selectionStart"in c&&yd(c)?c={start:c.selectionStart,end:c.selectionEnd}:(c=(c.ownerDocument&&c.ownerDocument.defaultView||window).getSelection(),c={anchorNode:c.anchorNode,anchorOffset:c.anchorOffset,focusNode:c.focusNode,focusOffset:c.focusOffset});return xb&&vb(xb,c)?null:(xb=c,a=J.getPooled(Hf.select,Ad,a,b),a.type="select",a.target=Wa,Qa(a),a)}function Yh(a){var b="";da.Children.forEach(a,function(a){null!=a&&(b+=a)});return b}function Bd(a,b){a=B({children:void 0},b);if(b=Yh(b.children))a.children=
|
||||
b;return a}function Xa(a,b,c,d){a=a.options;if(b){b={};for(var e=0;e<c.length;e++)b["$"+c[e]]=!0;for(c=0;c<a.length;c++)e=b.hasOwnProperty("$"+a[c].value),a[c].selected!==e&&(a[c].selected=e),e&&d&&(a[c].defaultSelected=!0)}else{c=""+ua(c);b=null;for(e=0;e<a.length;e++){if(a[e].value===c){a[e].selected=!0;d&&(a[e].defaultSelected=!0);return}null!==b||a[e].disabled||(b=a[e])}null!==b&&(b.selected=!0)}}function Cd(a,b){null!=b.dangerouslySetInnerHTML?n("91"):void 0;return B({},b,{value:void 0,defaultValue:void 0,
|
||||
children:""+a._wrapperState.initialValue})}function If(a,b){var c=b.value;null==c&&(c=b.defaultValue,b=b.children,null!=b&&(null!=c?n("92"):void 0,Array.isArray(b)&&(1>=b.length?void 0:n("93"),b=b[0]),c=b),null==c&&(c=""));a._wrapperState={initialValue:ua(c)}}function Jf(a,b){var c=ua(b.value),d=ua(b.defaultValue);null!=c&&(c=""+c,c!==a.value&&(a.value=c),null==b.defaultValue&&a.defaultValue!==c&&(a.defaultValue=c));null!=d&&(a.defaultValue=""+d)}function Kf(a){switch(a){case "svg":return"http://www.w3.org/2000/svg";
|
||||
case "math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}function Dd(a,b){return null==a||"http://www.w3.org/1999/xhtml"===a?Kf(b):"http://www.w3.org/2000/svg"===a&&"foreignObject"===b?"http://www.w3.org/1999/xhtml":a}function Lf(a,b,c){return null==b||"boolean"===typeof b||""===b?"":c||"number"!==typeof b||0===b||yb.hasOwnProperty(a)&&yb[a]?(""+b).trim():b+"px"}function Mf(a,b){a=a.style;for(var c in b)if(b.hasOwnProperty(c)){var d=0===c.indexOf("--"),
|
||||
e=Lf(c,b[c],d);"float"===c&&(c="cssFloat");d?a.setProperty(c,e):a[c]=e}}function Ed(a,b){b&&(Zh[a]&&(null!=b.children||null!=b.dangerouslySetInnerHTML?n("137",a,""):void 0),null!=b.dangerouslySetInnerHTML&&(null!=b.children?n("60"):void 0,"object"===typeof b.dangerouslySetInnerHTML&&"__html"in b.dangerouslySetInnerHTML?void 0:n("61")),null!=b.style&&"object"!==typeof b.style?n("62",""):void 0)}function Fd(a,b){if(-1===a.indexOf("-"))return"string"===typeof b.is;switch(a){case "annotation-xml":case "color-profile":case "font-face":case "font-face-src":case "font-face-uri":case "font-face-format":case "font-face-name":case "missing-glyph":return!1;
|
||||
default:return!0}}function ha(a,b){a=9===a.nodeType||11===a.nodeType?a:a.ownerDocument;var c=zf(a);b=$c[b];for(var d=0;d<b.length;d++){var e=b[d];if(!c.hasOwnProperty(e)||!c[e]){switch(e){case "scroll":pc("scroll",a);break;case "focus":case "blur":pc("focus",a);pc("blur",a);c.blur=!0;c.focus=!0;break;case "cancel":case "close":af(e)&&pc(e,a);break;case "invalid":case "submit":case "reset":break;default:-1===zb.indexOf(e)&&r(e,a)}c[e]=!0}}}function tc(){}function Nf(a,b){switch(a){case "button":case "input":case "select":case "textarea":return!!b.autoFocus}return!1}
|
||||
function Gd(a,b){return"textarea"===a||"option"===a||"noscript"===a||"string"===typeof b.children||"number"===typeof b.children||"object"===typeof b.dangerouslySetInnerHTML&&null!==b.dangerouslySetInnerHTML&&null!=b.dangerouslySetInnerHTML.__html}function $h(a,b,c,d,e,f){a[ec]=e;"input"===c&&"radio"===e.type&&null!=e.name&&mf(a,e);Fd(c,d);d=Fd(c,e);for(f=0;f<b.length;f+=2){var g=b[f],h=b[f+1];"style"===g?Mf(a,h):"dangerouslySetInnerHTML"===g?Of(a,h):"children"===g?Ab(a,h):rd(a,g,h,d)}switch(c){case "input":td(a,
|
||||
e);break;case "textarea":Jf(a,e);break;case "select":b=a._wrapperState.wasMultiple,a._wrapperState.wasMultiple=!!e.multiple,c=e.value,null!=c?Xa(a,!!e.multiple,c,!1):b!==!!e.multiple&&(null!=e.defaultValue?Xa(a,!!e.multiple,e.defaultValue,!0):Xa(a,!!e.multiple,e.multiple?[]:"",!1))}}function Hd(a){for(a=a.nextSibling;a&&1!==a.nodeType&&3!==a.nodeType;)a=a.nextSibling;return a}function Pf(a){for(a=a.firstChild;a&&1!==a.nodeType&&3!==a.nodeType;)a=a.nextSibling;return a}function D(a,b){0>Ya||(a.current=
|
||||
Id[Ya],Id[Ya]=null,Ya--)}function L(a,b,c){Ya++;Id[Ya]=a.current;a.current=b}function Za(a,b){var c=a.type.contextTypes;if(!c)return va;var d=a.stateNode;if(d&&d.__reactInternalMemoizedUnmaskedChildContext===b)return d.__reactInternalMemoizedMaskedChildContext;var e={},f;for(f in c)e[f]=b[f];d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=b,a.__reactInternalMemoizedMaskedChildContext=e);return e}function E(a){a=a.childContextTypes;return null!==a&&void 0!==a}function uc(a){D(M,a);
|
||||
D(F,a)}function Jd(a){D(M,a);D(F,a)}function Qf(a,b,c){F.current!==va?n("168"):void 0;L(F,b,a);L(M,c,a)}function Rf(a,b,c){var d=a.stateNode;a=b.childContextTypes;if("function"!==typeof d.getChildContext)return c;d=d.getChildContext();for(var e in d)e in a?void 0:n("108",sa(b)||"Unknown",e);return B({},c,d)}function vc(a){var b=a.stateNode;b=b&&b.__reactInternalMemoizedMergedChildContext||va;Fa=F.current;L(F,b,a);L(M,M.current,a);return!0}function Sf(a,b,c){var d=a.stateNode;d?void 0:n("169");c?(b=
|
||||
Rf(a,b,Fa),d.__reactInternalMemoizedMergedChildContext=b,D(M,a),D(F,a),L(F,b,a)):D(M,a);L(M,c,a)}function Tf(a){return function(b){try{return a(b)}catch(c){}}}function ai(a){if("undefined"===typeof __REACT_DEVTOOLS_GLOBAL_HOOK__)return!1;var b=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(b.isDisabled||!b.supportsFiber)return!0;try{var c=b.inject(a);Kd=Tf(function(a){return b.onCommitFiberRoot(c,a)});Ld=Tf(function(a){return b.onCommitFiberUnmount(c,a)})}catch(d){}return!0}function bi(a,b,c,d){this.tag=a;this.key=
|
||||
c;this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null;this.index=0;this.ref=null;this.pendingProps=b;this.contextDependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null;this.mode=d;this.effectTag=0;this.lastEffect=this.firstEffect=this.nextEffect=null;this.childExpirationTime=this.expirationTime=0;this.alternate=null}function Md(a){a=a.prototype;return!(!a||!a.isReactComponent)}function ci(a){if("function"===typeof a)return Md(a)?1:0;if(void 0!==a&&
|
||||
null!==a){a=a.$$typeof;if(a===od)return 11;if(a===pd)return 14}return 2}function Ga(a,b,c){c=a.alternate;null===c?(c=S(a.tag,b,a.key,a.mode),c.elementType=a.elementType,c.type=a.type,c.stateNode=a.stateNode,c.alternate=a,a.alternate=c):(c.pendingProps=b,c.effectTag=0,c.nextEffect=null,c.firstEffect=null,c.lastEffect=null);c.childExpirationTime=a.childExpirationTime;c.expirationTime=a.expirationTime;c.child=a.child;c.memoizedProps=a.memoizedProps;c.memoizedState=a.memoizedState;c.updateQueue=a.updateQueue;
|
||||
c.contextDependencies=a.contextDependencies;c.sibling=a.sibling;c.index=a.index;c.ref=a.ref;return c}function wc(a,b,c,d,e,f){var g=2;d=a;if("function"===typeof a)Md(a)&&(g=1);else if("string"===typeof a)g=5;else a:switch(a){case ta:return wa(c.children,e,f,b);case ld:return Uf(c,e|3,f,b);case md:return Uf(c,e|2,f,b);case lc:return a=S(12,c,b,e|4),a.elementType=lc,a.type=lc,a.expirationTime=f,a;case nd:return a=S(13,c,b,e),b=nd,a.elementType=b,a.type=b,a.expirationTime=f,a;default:if("object"===typeof a&&
|
||||
null!==a)switch(a.$$typeof){case ff:g=10;break a;case ef:g=9;break a;case od:g=11;break a;case pd:g=14;break a;case gf:g=16;d=null;break a}n("130",null==a?a:typeof a,"")}b=S(g,c,b,e);b.elementType=a;b.type=d;b.expirationTime=f;return b}function wa(a,b,c,d){a=S(7,a,d,b);a.expirationTime=c;return a}function Uf(a,b,c,d){a=S(8,a,d,b);b=0===(b&1)?md:ld;a.elementType=b;a.type=b;a.expirationTime=c;return a}function Nd(a,b,c){a=S(6,a,null,b);a.expirationTime=c;return a}function Od(a,b,c){b=S(4,null!==a.children?
|
||||
a.children:[],a.key,b);b.expirationTime=c;b.stateNode={containerInfo:a.containerInfo,pendingChildren:null,implementation:a.implementation};return b}function Bb(a,b){a.didError=!1;var c=a.earliestPendingTime;0===c?a.earliestPendingTime=a.latestPendingTime=b:c<b?a.earliestPendingTime=b:a.latestPendingTime>b&&(a.latestPendingTime=b);xc(b,a)}function di(a,b){a.didError=!1;if(0===b)a.earliestPendingTime=0,a.latestPendingTime=0,a.earliestSuspendedTime=0,a.latestSuspendedTime=0,a.latestPingedTime=0;else{b<
|
||||
a.latestPingedTime&&(a.latestPingedTime=0);var c=a.latestPendingTime;0!==c&&(c>b?a.earliestPendingTime=a.latestPendingTime=0:a.earliestPendingTime>b&&(a.earliestPendingTime=a.latestPendingTime));c=a.earliestSuspendedTime;0===c?Bb(a,b):b<a.latestSuspendedTime?(a.earliestSuspendedTime=0,a.latestSuspendedTime=0,a.latestPingedTime=0,Bb(a,b)):b>c&&Bb(a,b)}xc(0,a)}function Vf(a,b){a.didError=!1;a.latestPingedTime>=b&&(a.latestPingedTime=0);var c=a.earliestPendingTime,d=a.latestPendingTime;c===b?a.earliestPendingTime=
|
||||
d===b?a.latestPendingTime=0:d:d===b&&(a.latestPendingTime=c);c=a.earliestSuspendedTime;d=a.latestSuspendedTime;0===c?a.earliestSuspendedTime=a.latestSuspendedTime=b:c<b?a.earliestSuspendedTime=b:d>b&&(a.latestSuspendedTime=b);xc(b,a)}function Wf(a,b){var c=a.earliestPendingTime;a=a.earliestSuspendedTime;c>b&&(b=c);a>b&&(b=a);return b}function xc(a,b){var c=b.earliestSuspendedTime,d=b.latestSuspendedTime,e=b.earliestPendingTime,f=b.latestPingedTime;e=0!==e?e:f;0===e&&(0===a||d<a)&&(e=d);a=e;0!==a&&
|
||||
c>a&&(a=c);b.nextExpirationTimeToWorkOn=e;b.expirationTime=a}function P(a,b){if(a&&a.defaultProps){b=B({},b);a=a.defaultProps;for(var c in a)void 0===b[c]&&(b[c]=a[c])}return b}function ei(a){var b=a._result;switch(a._status){case 1:return b;case 2:throw b;case 0:throw b;default:a._status=0;b=a._ctor;b=b();b.then(function(b){0===a._status&&(b=b.default,a._status=1,a._result=b)},function(b){0===a._status&&(a._status=2,a._result=b)});switch(a._status){case 1:return a._result;case 2:throw a._result;
|
||||
}a._result=b;throw b;}}function yc(a,b,c,d){b=a.memoizedState;c=c(d,b);c=null===c||void 0===c?b:B({},b,c);a.memoizedState=c;d=a.updateQueue;null!==d&&0===a.expirationTime&&(d.baseState=c)}function Xf(a,b,c,d,e,f,g){a=a.stateNode;return"function"===typeof a.shouldComponentUpdate?a.shouldComponentUpdate(d,f,g):b.prototype&&b.prototype.isPureReactComponent?!vb(c,d)||!vb(e,f):!0}function Yf(a,b,c,d){var e=!1;d=va;var f=b.contextType;"object"===typeof f&&null!==f?f=T(f):(d=E(b)?Fa:F.current,e=b.contextTypes,
|
||||
f=(e=null!==e&&void 0!==e)?Za(a,d):va);b=new b(c,f);a.memoizedState=null!==b.state&&void 0!==b.state?b.state:null;b.updater=zc;a.stateNode=b;b._reactInternalFiber=a;e&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=d,a.__reactInternalMemoizedMaskedChildContext=f);return b}function Zf(a,b,c,d){a=b.state;"function"===typeof b.componentWillReceiveProps&&b.componentWillReceiveProps(c,d);"function"===typeof b.UNSAFE_componentWillReceiveProps&&b.UNSAFE_componentWillReceiveProps(c,d);b.state!==
|
||||
a&&zc.enqueueReplaceState(b,b.state,null)}function Pd(a,b,c,d){var e=a.stateNode;e.props=c;e.state=a.memoizedState;e.refs=$f;var f=b.contextType;"object"===typeof f&&null!==f?e.context=T(f):(f=E(b)?Fa:F.current,e.context=Za(a,f));f=a.updateQueue;null!==f&&(Cb(a,f,c,e,d),e.state=a.memoizedState);f=b.getDerivedStateFromProps;"function"===typeof f&&(yc(a,b,f,c),e.state=a.memoizedState);"function"===typeof b.getDerivedStateFromProps||"function"===typeof e.getSnapshotBeforeUpdate||"function"!==typeof e.UNSAFE_componentWillMount&&
|
||||
"function"!==typeof e.componentWillMount||(b=e.state,"function"===typeof e.componentWillMount&&e.componentWillMount(),"function"===typeof e.UNSAFE_componentWillMount&&e.UNSAFE_componentWillMount(),b!==e.state&&zc.enqueueReplaceState(e,e.state,null),f=a.updateQueue,null!==f&&(Cb(a,f,c,e,d),e.state=a.memoizedState));"function"===typeof e.componentDidMount&&(a.effectTag|=4)}function Db(a,b,c){a=c.ref;if(null!==a&&"function"!==typeof a&&"object"!==typeof a){if(c._owner){c=c._owner;var d=void 0;c&&(1!==
|
||||
c.tag?n("309"):void 0,d=c.stateNode);d?void 0:n("147",a);var e=""+a;if(null!==b&&null!==b.ref&&"function"===typeof b.ref&&b.ref._stringRef===e)return b.ref;b=function(a){var b=d.refs;b===$f&&(b=d.refs={});null===a?delete b[e]:b[e]=a};b._stringRef=e;return b}"string"!==typeof a?n("284"):void 0;c._owner?void 0:n("290",a)}return a}function Ac(a,b){"textarea"!==a.type&&n("31","[object Object]"===Object.prototype.toString.call(b)?"object with keys {"+Object.keys(b).join(", ")+"}":b,"")}function ag(a){function b(b,
|
||||
c){if(a){var d=b.lastEffect;null!==d?(d.nextEffect=c,b.lastEffect=c):b.firstEffect=b.lastEffect=c;c.nextEffect=null;c.effectTag=8}}function c(c,d){if(!a)return null;for(;null!==d;)b(c,d),d=d.sibling;return null}function d(a,b){for(a=new Map;null!==b;)null!==b.key?a.set(b.key,b):a.set(b.index,b),b=b.sibling;return a}function e(a,b,c){a=Ga(a,b,c);a.index=0;a.sibling=null;return a}function f(b,c,d){b.index=d;if(!a)return c;d=b.alternate;if(null!==d)return d=d.index,d<c?(b.effectTag=2,c):d;b.effectTag=
|
||||
2;return c}function g(b){a&&null===b.alternate&&(b.effectTag=2);return b}function h(a,b,c,d){if(null===b||6!==b.tag)return b=Nd(c,a.mode,d),b.return=a,b;b=e(b,c,d);b.return=a;return b}function l(a,b,c,d){if(null!==b&&b.elementType===c.type)return d=e(b,c.props,d),d.ref=Db(a,b,c),d.return=a,d;d=wc(c.type,c.key,c.props,null,a.mode,d);d.ref=Db(a,b,c);d.return=a;return d}function k(a,b,c,d){if(null===b||4!==b.tag||b.stateNode.containerInfo!==c.containerInfo||b.stateNode.implementation!==c.implementation)return b=
|
||||
Od(c,a.mode,d),b.return=a,b;b=e(b,c.children||[],d);b.return=a;return b}function m(a,b,c,d,f){if(null===b||7!==b.tag)return b=wa(c,a.mode,d,f),b.return=a,b;b=e(b,c,d);b.return=a;return b}function Ff(a,b,c){if("string"===typeof b||"number"===typeof b)return b=Nd(""+b,a.mode,c),b.return=a,b;if("object"===typeof b&&null!==b){switch(b.$$typeof){case Bc:return c=wc(b.type,b.key,b.props,null,a.mode,c),c.ref=Db(a,null,b),c.return=a,c;case Va:return b=Od(b,a.mode,c),b.return=a,b}if(Cc(b)||sb(b))return b=
|
||||
wa(b,a.mode,c,null),b.return=a,b;Ac(a,b)}return null}function p(a,b,c,d){var e=null!==b?b.key:null;if("string"===typeof c||"number"===typeof c)return null!==e?null:h(a,b,""+c,d);if("object"===typeof c&&null!==c){switch(c.$$typeof){case Bc:return c.key===e?c.type===ta?m(a,b,c.props.children,d,e):l(a,b,c,d):null;case Va:return c.key===e?k(a,b,c,d):null}if(Cc(c)||sb(c))return null!==e?null:m(a,b,c,d,null);Ac(a,c)}return null}function r(a,b,c,d,e){if("string"===typeof d||"number"===typeof d)return a=
|
||||
a.get(c)||null,h(b,a,""+d,e);if("object"===typeof d&&null!==d){switch(d.$$typeof){case Bc:return a=a.get(null===d.key?c:d.key)||null,d.type===ta?m(b,a,d.props.children,e,d.key):l(b,a,d,e);case Va:return a=a.get(null===d.key?c:d.key)||null,k(b,a,d,e)}if(Cc(d)||sb(d))return a=a.get(c)||null,m(b,a,d,e,null);Ac(b,d)}return null}function u(e,g,h,k){for(var l=null,m=null,n=g,q=g=0,v=null;null!==n&&q<h.length;q++){n.index>q?(v=n,n=null):v=n.sibling;var Q=p(e,n,h[q],k);if(null===Q){null===n&&(n=v);break}a&&
|
||||
n&&null===Q.alternate&&b(e,n);g=f(Q,g,q);null===m?l=Q:m.sibling=Q;m=Q;n=v}if(q===h.length)return c(e,n),l;if(null===n){for(;q<h.length;q++)if(n=Ff(e,h[q],k))g=f(n,g,q),null===m?l=n:m.sibling=n,m=n;return l}for(n=d(e,n);q<h.length;q++)if(v=r(n,e,q,h[q],k))a&&null!==v.alternate&&n.delete(null===v.key?q:v.key),g=f(v,g,q),null===m?l=v:m.sibling=v,m=v;a&&n.forEach(function(a){return b(e,a)});return l}function x(e,g,h,k){var l=sb(h);"function"!==typeof l?n("150"):void 0;h=l.call(h);null==h?n("151"):void 0;
|
||||
for(var m=l=null,q=g,v=g=0,Q=null,t=h.next();null!==q&&!t.done;v++,t=h.next()){q.index>v?(Q=q,q=null):Q=q.sibling;var u=p(e,q,t.value,k);if(null===u){q||(q=Q);break}a&&q&&null===u.alternate&&b(e,q);g=f(u,g,v);null===m?l=u:m.sibling=u;m=u;q=Q}if(t.done)return c(e,q),l;if(null===q){for(;!t.done;v++,t=h.next())t=Ff(e,t.value,k),null!==t&&(g=f(t,g,v),null===m?l=t:m.sibling=t,m=t);return l}for(q=d(e,q);!t.done;v++,t=h.next())t=r(q,e,v,t.value,k),null!==t&&(a&&null!==t.alternate&&q.delete(null===t.key?
|
||||
v:t.key),g=f(t,g,v),null===m?l=t:m.sibling=t,m=t);a&&q.forEach(function(a){return b(e,a)});return l}return function(a,d,f,h){var k="object"===typeof f&&null!==f&&f.type===ta&&null===f.key;k&&(f=f.props.children);var l="object"===typeof f&&null!==f;if(l)switch(f.$$typeof){case Bc:a:{l=f.key;for(k=d;null!==k;){if(k.key===l)if(7===k.tag?f.type===ta:k.elementType===f.type){c(a,k.sibling);d=e(k,f.type===ta?f.props.children:f.props,h);d.ref=Db(a,k,f);d.return=a;a=d;break a}else{c(a,k);break}else b(a,k);
|
||||
k=k.sibling}f.type===ta?(d=wa(f.props.children,a.mode,h,f.key),d.return=a,a=d):(h=wc(f.type,f.key,f.props,null,a.mode,h),h.ref=Db(a,d,f),h.return=a,a=h)}return g(a);case Va:a:{for(k=f.key;null!==d;){if(d.key===k)if(4===d.tag&&d.stateNode.containerInfo===f.containerInfo&&d.stateNode.implementation===f.implementation){c(a,d.sibling);d=e(d,f.children||[],h);d.return=a;a=d;break a}else{c(a,d);break}else b(a,d);d=d.sibling}d=Od(f,a.mode,h);d.return=a;a=d}return g(a)}if("string"===typeof f||"number"===
|
||||
typeof f)return f=""+f,null!==d&&6===d.tag?(c(a,d.sibling),d=e(d,f,h),d.return=a,a=d):(c(a,d),d=Nd(f,a.mode,h),d.return=a,a=d),g(a);if(Cc(f))return u(a,d,f,h);if(sb(f))return x(a,d,f,h);l&&Ac(a,f);if("undefined"===typeof f&&!k)switch(a.tag){case 1:case 0:h=a.type,n("152",h.displayName||h.name||"Component")}return c(a,d)}}function Ha(a){a===Eb?n("174"):void 0;return a}function Qd(a,b){L(Fb,b,a);L(Gb,a,a);L(U,Eb,a);var c=b.nodeType;switch(c){case 9:case 11:b=(b=b.documentElement)?b.namespaceURI:Dd(null,
|
||||
"");break;default:c=8===c?b.parentNode:b,b=c.namespaceURI||null,c=c.tagName,b=Dd(b,c)}D(U,a);L(U,b,a)}function $a(a){D(U,a);D(Gb,a);D(Fb,a)}function bg(a){Ha(Fb.current);var b=Ha(U.current);var c=Dd(b,a.type);b!==c&&(L(Gb,a,a),L(U,c,a))}function Rd(a){Gb.current===a&&(D(U,a),D(Gb,a))}function V(){n("321")}function Sd(a,b){if(null===b)return!1;for(var c=0;c<b.length&&c<a.length;c++)if(!Ea(a[c],b[c]))return!1;return!0}function Td(a,b,c,d,e,f){Hb=f;xa=b;W=null!==a?a.memoizedState:null;Dc.current=null===
|
||||
W?fi:cg;b=c(d,e);if(Ib){do Ib=!1,Jb+=1,W=null!==a?a.memoizedState:null,ab=bb,X=G=y=null,Dc.current=cg,b=c(d,e);while(Ib);ia=null;Jb=0}Dc.current=Ud;a=xa;a.memoizedState=bb;a.expirationTime=Kb;a.updateQueue=X;a.effectTag|=Lb;a=null!==y&&null!==y.next;Hb=0;ab=G=bb=W=y=xa=null;Kb=0;X=null;Lb=0;a?n("300"):void 0;return b}function Vd(){Dc.current=Ud;Hb=0;ab=G=bb=W=y=xa=null;Kb=0;X=null;Lb=0;Ib=!1;ia=null;Jb=0}function cb(){var a={memoizedState:null,baseState:null,queue:null,baseUpdate:null,next:null};
|
||||
null===G?bb=G=a:G=G.next=a;return G}function Mb(){if(null!==ab)G=ab,ab=G.next,y=W,W=null!==y?y.next:null;else{null===W?n("310"):void 0;y=W;var a={memoizedState:y.memoizedState,baseState:y.baseState,queue:y.queue,baseUpdate:y.baseUpdate,next:null};G=null===G?bb=a:G.next=a;W=y.next}return G}function dg(a,b){return"function"===typeof b?b(a):b}function eg(a,b,c){b=Mb();c=b.queue;null===c?n("311"):void 0;c.lastRenderedReducer=a;if(0<Jb){var d=c.dispatch;if(null!==ia){var e=ia.get(c);if(void 0!==e){ia.delete(c);
|
||||
var f=b.memoizedState;do f=a(f,e.action),e=e.next;while(null!==e);Ea(f,b.memoizedState)||(ja=!0);b.memoizedState=f;b.baseUpdate===c.last&&(b.baseState=f);c.lastRenderedState=f;return[f,d]}}return[b.memoizedState,d]}d=c.last;var g=b.baseUpdate;f=b.baseState;null!==g?(null!==d&&(d.next=null),d=g.next):d=null!==d?d.next:null;if(null!==d){var h=e=null,l=d,k=!1;do{var m=l.expirationTime;m<Hb?(k||(k=!0,h=g,e=f),m>Kb&&(Kb=m)):f=l.eagerReducer===a?l.eagerState:a(f,l.action);g=l;l=l.next}while(null!==l&&l!==
|
||||
d);k||(h=g,e=f);Ea(f,b.memoizedState)||(ja=!0);b.memoizedState=f;b.baseUpdate=h;b.baseState=e;c.lastRenderedState=f}return[b.memoizedState,c.dispatch]}function Wd(a,b,c,d){a={tag:a,create:b,destroy:c,deps:d,next:null};null===X?(X={lastEffect:null},X.lastEffect=a.next=a):(b=X.lastEffect,null===b?X.lastEffect=a.next=a:(c=b.next,b.next=a,a.next=c,X.lastEffect=a));return a}function Xd(a,b,c,d){var e=cb();Lb|=a;e.memoizedState=Wd(b,c,void 0,void 0===d?null:d)}function Yd(a,b,c,d){var e=Mb();d=void 0===
|
||||
d?null:d;var f=void 0;if(null!==y){var g=y.memoizedState;f=g.destroy;if(null!==d&&Sd(d,g.deps)){Wd(db,c,f,d);return}}Lb|=a;e.memoizedState=Wd(b,c,f,d)}function fg(a,b){if("function"===typeof b)return a=a(),b(a),function(){b(null)};if(null!==b&&void 0!==b)return a=a(),b.current=a,function(){b.current=null}}function gg(a,b){}function hg(a,b,c){25>Jb?void 0:n("301");var d=a.alternate;if(a===xa||null!==d&&d===xa)if(Ib=!0,a={expirationTime:Hb,action:c,eagerReducer:null,eagerState:null,next:null},null===
|
||||
ia&&(ia=new Map),c=ia.get(b),void 0===c)ia.set(b,a);else{for(b=c;null!==b.next;)b=b.next;b.next=a}else{eb();var e=ka();e=fb(e,a);var f={expirationTime:e,action:c,eagerReducer:null,eagerState:null,next:null},g=b.last;if(null===g)f.next=f;else{var h=g.next;null!==h&&(f.next=h);g.next=f}b.last=f;if(0===a.expirationTime&&(null===d||0===d.expirationTime)&&(d=b.lastRenderedReducer,null!==d))try{var l=b.lastRenderedState,k=d(l,c);f.eagerReducer=d;f.eagerState=k;if(Ea(k,l))return}catch(m){}finally{}ya(a,
|
||||
e)}}function ig(a,b){var c=S(5,null,null,0);c.elementType="DELETED";c.type="DELETED";c.stateNode=b;c.return=a;c.effectTag=8;null!==a.lastEffect?(a.lastEffect.nextEffect=c,a.lastEffect=c):a.firstEffect=a.lastEffect=c}function jg(a,b){switch(a.tag){case 5:var c=a.type;b=1!==b.nodeType||c.toLowerCase()!==b.nodeName.toLowerCase()?null:b;return null!==b?(a.stateNode=b,!0):!1;case 6:return b=""===a.pendingProps||3!==b.nodeType?null:b,null!==b?(a.stateNode=b,!0):!1;case 13:return!1;default:return!1}}function kg(a){if(Ia){var b=
|
||||
gb;if(b){var c=b;if(!jg(a,b)){b=Hd(c);if(!b||!jg(a,b)){a.effectTag|=2;Ia=!1;la=a;return}ig(la,c)}la=a;gb=Pf(b)}else a.effectTag|=2,Ia=!1,la=a}}function lg(a){for(a=a.return;null!==a&&5!==a.tag&&3!==a.tag&&18!==a.tag;)a=a.return;la=a}function Zd(a){if(a!==la)return!1;if(!Ia)return lg(a),Ia=!0,!1;var b=a.type;if(5!==a.tag||"head"!==b&&"body"!==b&&!Gd(b,a.memoizedProps))for(b=gb;b;)ig(a,b),b=Hd(b);lg(a);gb=la?Hd(a.stateNode):null;return!0}function $d(){gb=la=null;Ia=!1}function N(a,b,c,d){b.child=null===
|
||||
a?ae(b,null,c,d):hb(b,a.child,c,d)}function mg(a,b,c,d,e){c=c.render;var f=b.ref;ib(b,e);d=Td(a,b,c,d,f,e);if(null!==a&&!ja)return b.updateQueue=a.updateQueue,b.effectTag&=-517,a.expirationTime<=e&&(a.expirationTime=0),ma(a,b,e);b.effectTag|=1;N(a,b,d,e);return b.child}function ng(a,b,c,d,e,f){if(null===a){var g=c.type;if("function"===typeof g&&!Md(g)&&void 0===g.defaultProps&&null===c.compare&&void 0===c.defaultProps)return b.tag=15,b.type=g,og(a,b,g,d,e,f);a=wc(c.type,null,d,null,b.mode,f);a.ref=
|
||||
b.ref;a.return=b;return b.child=a}g=a.child;if(e<f&&(e=g.memoizedProps,c=c.compare,c=null!==c?c:vb,c(e,d)&&a.ref===b.ref))return ma(a,b,f);b.effectTag|=1;a=Ga(g,d,f);a.ref=b.ref;a.return=b;return b.child=a}function og(a,b,c,d,e,f){return null!==a&&vb(a.memoizedProps,d)&&a.ref===b.ref&&(ja=!1,e<f)?ma(a,b,f):be(a,b,c,d,f)}function pg(a,b){var c=b.ref;if(null===a&&null!==c||null!==a&&a.ref!==c)b.effectTag|=128}function be(a,b,c,d,e){var f=E(c)?Fa:F.current;f=Za(b,f);ib(b,e);c=Td(a,b,c,d,f,e);if(null!==
|
||||
a&&!ja)return b.updateQueue=a.updateQueue,b.effectTag&=-517,a.expirationTime<=e&&(a.expirationTime=0),ma(a,b,e);b.effectTag|=1;N(a,b,c,e);return b.child}function qg(a,b,c,d,e){if(E(c)){var f=!0;vc(b)}else f=!1;ib(b,e);if(null===b.stateNode)null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2),Yf(b,c,d,e),Pd(b,c,d,e),d=!0;else if(null===a){var g=b.stateNode,h=b.memoizedProps;g.props=h;var l=g.context,k=c.contextType;"object"===typeof k&&null!==k?k=T(k):(k=E(c)?Fa:F.current,k=Za(b,k));var m=
|
||||
c.getDerivedStateFromProps,n="function"===typeof m||"function"===typeof g.getSnapshotBeforeUpdate;n||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==d||l!==k)&&Zf(b,g,d,k);za=!1;var p=b.memoizedState;l=g.state=p;var r=b.updateQueue;null!==r&&(Cb(b,r,d,g,e),l=b.memoizedState);h!==d||p!==l||M.current||za?("function"===typeof m&&(yc(b,c,m,d),l=b.memoizedState),(h=za||Xf(b,c,h,d,p,l,k))?(n||"function"!==typeof g.UNSAFE_componentWillMount&&
|
||||
"function"!==typeof g.componentWillMount||("function"===typeof g.componentWillMount&&g.componentWillMount(),"function"===typeof g.UNSAFE_componentWillMount&&g.UNSAFE_componentWillMount()),"function"===typeof g.componentDidMount&&(b.effectTag|=4)):("function"===typeof g.componentDidMount&&(b.effectTag|=4),b.memoizedProps=d,b.memoizedState=l),g.props=d,g.state=l,g.context=k,d=h):("function"===typeof g.componentDidMount&&(b.effectTag|=4),d=!1)}else g=b.stateNode,h=b.memoizedProps,g.props=b.type===b.elementType?
|
||||
h:P(b.type,h),l=g.context,k=c.contextType,"object"===typeof k&&null!==k?k=T(k):(k=E(c)?Fa:F.current,k=Za(b,k)),m=c.getDerivedStateFromProps,(n="function"===typeof m||"function"===typeof g.getSnapshotBeforeUpdate)||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==d||l!==k)&&Zf(b,g,d,k),za=!1,l=b.memoizedState,p=g.state=l,r=b.updateQueue,null!==r&&(Cb(b,r,d,g,e),p=b.memoizedState),h!==d||l!==p||M.current||za?("function"===typeof m&&(yc(b,
|
||||
c,m,d),p=b.memoizedState),(m=za||Xf(b,c,h,d,l,p,k))?(n||"function"!==typeof g.UNSAFE_componentWillUpdate&&"function"!==typeof g.componentWillUpdate||("function"===typeof g.componentWillUpdate&&g.componentWillUpdate(d,p,k),"function"===typeof g.UNSAFE_componentWillUpdate&&g.UNSAFE_componentWillUpdate(d,p,k)),"function"===typeof g.componentDidUpdate&&(b.effectTag|=4),"function"===typeof g.getSnapshotBeforeUpdate&&(b.effectTag|=256)):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&l===
|
||||
a.memoizedState||(b.effectTag|=4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&l===a.memoizedState||(b.effectTag|=256),b.memoizedProps=d,b.memoizedState=p),g.props=d,g.state=p,g.context=k,d=m):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&l===a.memoizedState||(b.effectTag|=4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&l===a.memoizedState||(b.effectTag|=256),d=!1);return ce(a,b,c,d,f,e)}function ce(a,b,c,d,e,f){pg(a,b);var g=0!==(b.effectTag&
|
||||
64);if(!d&&!g)return e&&Sf(b,c,!1),ma(a,b,f);d=b.stateNode;gi.current=b;var h=g&&"function"!==typeof c.getDerivedStateFromError?null:d.render();b.effectTag|=1;null!==a&&g?(b.child=hb(b,a.child,null,f),b.child=hb(b,null,h,f)):N(a,b,h,f);b.memoizedState=d.state;e&&Sf(b,c,!0);return b.child}function rg(a){var b=a.stateNode;b.pendingContext?Qf(a,b.pendingContext,b.pendingContext!==b.context):b.context&&Qf(a,b.context,!1);Qd(a,b.containerInfo)}function sg(a,b,c){var d=b.mode,e=b.pendingProps,f=b.memoizedState;
|
||||
if(0===(b.effectTag&64)){f=null;var g=!1}else f={timedOutAt:null!==f?f.timedOutAt:0},g=!0,b.effectTag&=-65;if(null===a)if(g){var h=e.fallback;a=wa(null,d,0,null);0===(b.mode&1)&&(a.child=null!==b.memoizedState?b.child.child:b.child);d=wa(h,d,c,null);a.sibling=d;c=a;c.return=d.return=b}else c=d=ae(b,null,e.children,c);else null!==a.memoizedState?(d=a.child,h=d.sibling,g?(c=e.fallback,e=Ga(d,d.pendingProps,0),0===(b.mode&1)&&(g=null!==b.memoizedState?b.child.child:b.child,g!==d.child&&(e.child=g)),
|
||||
d=e.sibling=Ga(h,c,h.expirationTime),c=e,e.childExpirationTime=0,c.return=d.return=b):c=d=hb(b,d.child,e.children,c)):(h=a.child,g?(g=e.fallback,e=wa(null,d,0,null),e.child=h,0===(b.mode&1)&&(e.child=null!==b.memoizedState?b.child.child:b.child),d=e.sibling=wa(g,d,c,null),d.effectTag|=2,c=e,e.childExpirationTime=0,c.return=d.return=b):d=c=hb(b,h,e.children,c)),b.stateNode=a.stateNode;b.memoizedState=f;b.child=c;return d}function ma(a,b,c){null!==a&&(b.contextDependencies=a.contextDependencies);if(b.childExpirationTime<
|
||||
c)return null;null!==a&&b.child!==a.child?n("153"):void 0;if(null!==b.child){a=b.child;c=Ga(a,a.pendingProps,a.expirationTime);b.child=c;for(c.return=b;null!==a.sibling;)a=a.sibling,c=c.sibling=Ga(a,a.pendingProps,a.expirationTime),c.return=b;c.sibling=null}return b.child}function hi(a,b,c){var d=b.expirationTime;if(null!==a)if(a.memoizedProps!==b.pendingProps||M.current)ja=!0;else{if(d<c){ja=!1;switch(b.tag){case 3:rg(b);$d();break;case 5:bg(b);break;case 1:E(b.type)&&vc(b);break;case 4:Qd(b,b.stateNode.containerInfo);
|
||||
break;case 10:tg(b,b.memoizedProps.value);break;case 13:if(null!==b.memoizedState){d=b.child.childExpirationTime;if(0!==d&&d>=c)return sg(a,b,c);b=ma(a,b,c);return null!==b?b.sibling:null}}return ma(a,b,c)}}else ja=!1;b.expirationTime=0;switch(b.tag){case 2:d=b.elementType;null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2);a=b.pendingProps;var e=Za(b,F.current);ib(b,c);e=Td(null,b,d,a,e,c);b.effectTag|=1;if("object"===typeof e&&null!==e&&"function"===typeof e.render&&void 0===e.$$typeof){b.tag=
|
||||
1;Vd();if(E(d)){var f=!0;vc(b)}else f=!1;b.memoizedState=null!==e.state&&void 0!==e.state?e.state:null;var g=d.getDerivedStateFromProps;"function"===typeof g&&yc(b,d,g,a);e.updater=zc;b.stateNode=e;e._reactInternalFiber=b;Pd(b,d,a,c);b=ce(null,b,d,!0,f,c)}else b.tag=0,N(null,b,e,c),b=b.child;return b;case 16:e=b.elementType;null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2);f=b.pendingProps;a=ei(e);b.type=a;e=b.tag=ci(a);f=P(a,f);g=void 0;switch(e){case 0:g=be(null,b,a,f,c);break;case 1:g=
|
||||
qg(null,b,a,f,c);break;case 11:g=mg(null,b,a,f,c);break;case 14:g=ng(null,b,a,P(a.type,f),d,c);break;default:n("306",a,"")}return g;case 0:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:P(d,e),be(a,b,d,e,c);case 1:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:P(d,e),qg(a,b,d,e,c);case 3:rg(b);d=b.updateQueue;null===d?n("282"):void 0;e=b.memoizedState;e=null!==e?e.element:null;Cb(b,d,b.pendingProps,null,c);d=b.memoizedState.element;if(d===e)$d(),b=ma(a,b,c);else{e=b.stateNode;if(e=
|
||||
(null===a||null===a.child)&&e.hydrate)gb=Pf(b.stateNode.containerInfo),la=b,e=Ia=!0;e?(b.effectTag|=2,b.child=ae(b,null,d,c)):(N(a,b,d,c),$d());b=b.child}return b;case 5:return bg(b),null===a&&kg(b),d=b.type,e=b.pendingProps,f=null!==a?a.memoizedProps:null,g=e.children,Gd(d,e)?g=null:null!==f&&Gd(d,f)&&(b.effectTag|=16),pg(a,b),1!==c&&b.mode&1&&e.hidden?(b.expirationTime=b.childExpirationTime=1,b=null):(N(a,b,g,c),b=b.child),b;case 6:return null===a&&kg(b),null;case 13:return sg(a,b,c);case 4:return Qd(b,
|
||||
b.stateNode.containerInfo),d=b.pendingProps,null===a?b.child=hb(b,null,d,c):N(a,b,d,c),b.child;case 11:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:P(d,e),mg(a,b,d,e,c);case 7:return N(a,b,b.pendingProps,c),b.child;case 8:return N(a,b,b.pendingProps.children,c),b.child;case 12:return N(a,b,b.pendingProps.children,c),b.child;case 10:a:{d=b.type._context;e=b.pendingProps;g=b.memoizedProps;f=e.value;tg(b,f);if(null!==g){var h=g.value;f=Ea(h,f)?0:("function"===typeof d._calculateChangedBits?
|
||||
d._calculateChangedBits(h,f):1073741823)|0;if(0===f){if(g.children===e.children&&!M.current){b=ma(a,b,c);break a}}else for(h=b.child,null!==h&&(h.return=b);null!==h;){var l=h.contextDependencies;if(null!==l){g=h.child;for(var k=l.first;null!==k;){if(k.context===d&&0!==(k.observedBits&f)){1===h.tag&&(k=Aa(c),k.tag=Ec,na(h,k));h.expirationTime<c&&(h.expirationTime=c);k=h.alternate;null!==k&&k.expirationTime<c&&(k.expirationTime=c);k=c;for(var m=h.return;null!==m;){var p=m.alternate;if(m.childExpirationTime<
|
||||
k)m.childExpirationTime=k,null!==p&&p.childExpirationTime<k&&(p.childExpirationTime=k);else if(null!==p&&p.childExpirationTime<k)p.childExpirationTime=k;else break;m=m.return}l.expirationTime<c&&(l.expirationTime=c);break}k=k.next}}else g=10===h.tag?h.type===b.type?null:h.child:h.child;if(null!==g)g.return=h;else for(g=h;null!==g;){if(g===b){g=null;break}h=g.sibling;if(null!==h){h.return=g.return;g=h;break}g=g.return}h=g}}N(a,b,e.children,c);b=b.child}return b;case 9:return e=b.type,f=b.pendingProps,
|
||||
d=f.children,ib(b,c),e=T(e,f.unstable_observedBits),d=d(e),b.effectTag|=1,N(a,b,d,c),b.child;case 14:return e=b.type,f=P(e,b.pendingProps),f=P(e.type,f),ng(a,b,e,f,d,c);case 15:return og(a,b,b.type,b.pendingProps,d,c);case 17:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:P(d,e),null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2),b.tag=1,E(d)?(a=!0,vc(b)):a=!1,ib(b,c),Yf(b,d,e,c),Pd(b,d,e,c),ce(null,b,d,!0,a,c)}n("156")}function tg(a,b){var c=a.type._context;L(de,c._currentValue,
|
||||
a);c._currentValue=b}function ee(a){var b=de.current;D(de,a);a.type._context._currentValue=b}function ib(a,b){Nb=a;Ob=Ja=null;var c=a.contextDependencies;null!==c&&c.expirationTime>=b&&(ja=!0);a.contextDependencies=null}function T(a,b){if(Ob!==a&&!1!==b&&0!==b){if("number"!==typeof b||1073741823===b)Ob=a,b=1073741823;b={context:a,observedBits:b,next:null};null===Ja?(null===Nb?n("308"):void 0,Ja=b,Nb.contextDependencies={first:b,expirationTime:0}):Ja=Ja.next=b}return a._currentValue}function Fc(a){return{baseState:a,
|
||||
firstUpdate:null,lastUpdate:null,firstCapturedUpdate:null,lastCapturedUpdate:null,firstEffect:null,lastEffect:null,firstCapturedEffect:null,lastCapturedEffect:null}}function fe(a){return{baseState:a.baseState,firstUpdate:a.firstUpdate,lastUpdate:a.lastUpdate,firstCapturedUpdate:null,lastCapturedUpdate:null,firstEffect:null,lastEffect:null,firstCapturedEffect:null,lastCapturedEffect:null}}function Aa(a){return{expirationTime:a,tag:ug,payload:null,callback:null,next:null,nextEffect:null}}function Gc(a,
|
||||
b){null===a.lastUpdate?a.firstUpdate=a.lastUpdate=b:(a.lastUpdate.next=b,a.lastUpdate=b)}function na(a,b){var c=a.alternate;if(null===c){var d=a.updateQueue;var e=null;null===d&&(d=a.updateQueue=Fc(a.memoizedState))}else d=a.updateQueue,e=c.updateQueue,null===d?null===e?(d=a.updateQueue=Fc(a.memoizedState),e=c.updateQueue=Fc(c.memoizedState)):d=a.updateQueue=fe(e):null===e&&(e=c.updateQueue=fe(d));null===e||d===e?Gc(d,b):null===d.lastUpdate||null===e.lastUpdate?(Gc(d,b),Gc(e,b)):(Gc(d,b),e.lastUpdate=
|
||||
b)}function vg(a,b){var c=a.updateQueue;c=null===c?a.updateQueue=Fc(a.memoizedState):wg(a,c);null===c.lastCapturedUpdate?c.firstCapturedUpdate=c.lastCapturedUpdate=b:(c.lastCapturedUpdate.next=b,c.lastCapturedUpdate=b)}function wg(a,b){var c=a.alternate;null!==c&&b===c.updateQueue&&(b=a.updateQueue=fe(b));return b}function xg(a,b,c,d,e,f){switch(c.tag){case yg:return a=c.payload,"function"===typeof a?a.call(f,d,e):a;case ge:a.effectTag=a.effectTag&-2049|64;case ug:a=c.payload;e="function"===typeof a?
|
||||
a.call(f,d,e):a;if(null===e||void 0===e)break;return B({},d,e);case Ec:za=!0}return d}function Cb(a,b,c,d,e){za=!1;b=wg(a,b);for(var f=b.baseState,g=null,h=0,l=b.firstUpdate,k=f;null!==l;){var m=l.expirationTime;m<e?(null===g&&(g=l,f=k),h<m&&(h=m)):(k=xg(a,b,l,k,c,d),null!==l.callback&&(a.effectTag|=32,l.nextEffect=null,null===b.lastEffect?b.firstEffect=b.lastEffect=l:(b.lastEffect.nextEffect=l,b.lastEffect=l)));l=l.next}m=null;for(l=b.firstCapturedUpdate;null!==l;){var n=l.expirationTime;n<e?(null===
|
||||
m&&(m=l,null===g&&(f=k)),h<n&&(h=n)):(k=xg(a,b,l,k,c,d),null!==l.callback&&(a.effectTag|=32,l.nextEffect=null,null===b.lastCapturedEffect?b.firstCapturedEffect=b.lastCapturedEffect=l:(b.lastCapturedEffect.nextEffect=l,b.lastCapturedEffect=l)));l=l.next}null===g&&(b.lastUpdate=null);null===m?b.lastCapturedUpdate=null:a.effectTag|=32;null===g&&null===m&&(f=k);b.baseState=f;b.firstUpdate=g;b.firstCapturedUpdate=m;a.expirationTime=h;a.memoizedState=k}function zg(a,b,c,d){null!==b.firstCapturedUpdate&&
|
||||
(null!==b.lastUpdate&&(b.lastUpdate.next=b.firstCapturedUpdate,b.lastUpdate=b.lastCapturedUpdate),b.firstCapturedUpdate=b.lastCapturedUpdate=null);Ag(b.firstEffect,c);b.firstEffect=b.lastEffect=null;Ag(b.firstCapturedEffect,c);b.firstCapturedEffect=b.lastCapturedEffect=null}function Ag(a,b){for(;null!==a;){var c=a.callback;if(null!==c){a.callback=null;var d=b;"function"!==typeof c?n("191",c):void 0;c.call(d)}a=a.nextEffect}}function Hc(a,b){return{value:a,source:b,stack:qd(b)}}function Pb(a){a.effectTag|=
|
||||
4}function Bg(a,b){var c=b.source,d=b.stack;null===d&&null!==c&&(d=qd(c));null!==c&&sa(c.type);b=b.value;null!==a&&1===a.tag&&sa(a.type);try{console.error(b)}catch(e){setTimeout(function(){throw e;})}}function Cg(a){var b=a.ref;if(null!==b)if("function"===typeof b)try{b(null)}catch(c){Ka(a,c)}else b.current=null}function Qb(a,b,c){c=c.updateQueue;c=null!==c?c.lastEffect:null;if(null!==c){var d=c=c.next;do{if((d.tag&a)!==db){var e=d.destroy;d.destroy=void 0;void 0!==e&&e()}(d.tag&b)!==db&&(e=d.create,
|
||||
d.destroy=e());d=d.next}while(d!==c)}}function ii(a,b){for(var c=a;;){if(5===c.tag){var d=c.stateNode;if(b)d.style.display="none";else{d=c.stateNode;var e=c.memoizedProps.style;e=void 0!==e&&null!==e&&e.hasOwnProperty("display")?e.display:null;d.style.display=Lf("display",e)}}else if(6===c.tag)c.stateNode.nodeValue=b?"":c.memoizedProps;else if(13===c.tag&&null!==c.memoizedState){d=c.child.sibling;d.return=c;c=d;continue}else if(null!==c.child){c.child.return=c;c=c.child;continue}if(c===a)break;for(;null===
|
||||
c.sibling;){if(null===c.return||c.return===a)return;c=c.return}c.sibling.return=c.return;c=c.sibling}}function Dg(a){"function"===typeof Ld&&Ld(a);switch(a.tag){case 0:case 11:case 14:case 15:var b=a.updateQueue;if(null!==b&&(b=b.lastEffect,null!==b)){var c=b=b.next;do{var d=c.destroy;if(void 0!==d){var e=a;try{d()}catch(f){Ka(e,f)}}c=c.next}while(c!==b)}break;case 1:Cg(a);b=a.stateNode;if("function"===typeof b.componentWillUnmount)try{b.props=a.memoizedProps,b.state=a.memoizedState,b.componentWillUnmount()}catch(f){Ka(a,
|
||||
f)}break;case 5:Cg(a);break;case 4:Eg(a)}}function Fg(a){return 5===a.tag||3===a.tag||4===a.tag}function Gg(a){a:{for(var b=a.return;null!==b;){if(Fg(b)){var c=b;break a}b=b.return}n("160");c=void 0}var d=b=void 0;switch(c.tag){case 5:b=c.stateNode;d=!1;break;case 3:b=c.stateNode.containerInfo;d=!0;break;case 4:b=c.stateNode.containerInfo;d=!0;break;default:n("161")}c.effectTag&16&&(Ab(b,""),c.effectTag&=-17);a:b:for(c=a;;){for(;null===c.sibling;){if(null===c.return||Fg(c.return)){c=null;break a}c=
|
||||
c.return}c.sibling.return=c.return;for(c=c.sibling;5!==c.tag&&6!==c.tag&&18!==c.tag;){if(c.effectTag&2)continue b;if(null===c.child||4===c.tag)continue b;else c.child.return=c,c=c.child}if(!(c.effectTag&2)){c=c.stateNode;break a}}for(var e=a;;){if(5===e.tag||6===e.tag)if(c)if(d){var f=b,g=e.stateNode,h=c;8===f.nodeType?f.parentNode.insertBefore(g,h):f.insertBefore(g,h)}else b.insertBefore(e.stateNode,c);else d?(g=b,h=e.stateNode,8===g.nodeType?(f=g.parentNode,f.insertBefore(h,g)):(f=g,f.appendChild(h)),
|
||||
g=g._reactRootContainer,null!==g&&void 0!==g||null!==f.onclick||(f.onclick=tc)):b.appendChild(e.stateNode);else if(4!==e.tag&&null!==e.child){e.child.return=e;e=e.child;continue}if(e===a)break;for(;null===e.sibling;){if(null===e.return||e.return===a)return;e=e.return}e.sibling.return=e.return;e=e.sibling}}function Eg(a){for(var b=a,c=!1,d=void 0,e=void 0;;){if(!c){c=b.return;a:for(;;){null===c?n("160"):void 0;switch(c.tag){case 5:d=c.stateNode;e=!1;break a;case 3:d=c.stateNode.containerInfo;e=!0;
|
||||
break a;case 4:d=c.stateNode.containerInfo;e=!0;break a}c=c.return}c=!0}if(5===b.tag||6===b.tag){a:for(var f=b,g=f;;)if(Dg(g),null!==g.child&&4!==g.tag)g.child.return=g,g=g.child;else{if(g===f)break;for(;null===g.sibling;){if(null===g.return||g.return===f)break a;g=g.return}g.sibling.return=g.return;g=g.sibling}e?(f=d,g=b.stateNode,8===f.nodeType?f.parentNode.removeChild(g):f.removeChild(g)):d.removeChild(b.stateNode)}else if(4===b.tag){if(null!==b.child){d=b.stateNode.containerInfo;e=!0;b.child.return=
|
||||
b;b=b.child;continue}}else if(Dg(b),null!==b.child){b.child.return=b;b=b.child;continue}if(b===a)break;for(;null===b.sibling;){if(null===b.return||b.return===a)return;b=b.return;4===b.tag&&(c=!1)}b.sibling.return=b.return;b=b.sibling}}function Hg(a,b){switch(b.tag){case 0:case 11:case 14:case 15:Qb(Rb,ji,b);break;case 1:break;case 5:var c=b.stateNode;if(null!=c){var d=b.memoizedProps;a=null!==a?a.memoizedProps:d;var e=b.type,f=b.updateQueue;b.updateQueue=null;null!==f&&$h(c,f,e,a,d,b)}break;case 6:null===
|
||||
b.stateNode?n("162"):void 0;b.stateNode.nodeValue=b.memoizedProps;break;case 3:break;case 12:break;case 13:c=b.memoizedState;d=void 0;a=b;null===c?d=!1:(d=!0,a=b.child,0===c.timedOutAt&&(c.timedOutAt=ka()));null!==a&&ii(a,d);c=b.updateQueue;if(null!==c){b.updateQueue=null;var g=b.stateNode;null===g&&(g=b.stateNode=new ki);c.forEach(function(a){var c=li.bind(null,b,a);g.has(a)||(g.add(a),a.then(c,c))})}break;case 17:break;default:n("163")}}function he(a,b,c){c=Aa(c);c.tag=ge;c.payload={element:null};
|
||||
var d=b.value;c.callback=function(){ie(d);Bg(a,b)};return c}function Ig(a,b,c){c=Aa(c);c.tag=ge;var d=a.type.getDerivedStateFromError;if("function"===typeof d){var e=b.value;c.payload=function(){return d(e)}}var f=a.stateNode;null!==f&&"function"===typeof f.componentDidCatch&&(c.callback=function(){"function"!==typeof d&&(null===Ba?Ba=new Set([this]):Ba.add(this));var c=b.value,e=b.stack;Bg(a,b);this.componentDidCatch(c,{componentStack:null!==e?e:""})});return c}function mi(a,b){switch(a.tag){case 1:return E(a.type)&&
|
||||
uc(a),b=a.effectTag,b&2048?(a.effectTag=b&-2049|64,a):null;case 3:return $a(a),Jd(a),b=a.effectTag,0!==(b&64)?n("285"):void 0,a.effectTag=b&-2049|64,a;case 5:return Rd(a),null;case 13:return b=a.effectTag,b&2048?(a.effectTag=b&-2049|64,a):null;case 18:return null;case 4:return $a(a),null;case 10:return ee(a),null;default:return null}}function Jg(){if(null!==x)for(var a=x.return;null!==a;){var b=a;switch(b.tag){case 1:var c=b.type.childContextTypes;null!==c&&void 0!==c&&uc(b);break;case 3:$a(b);Jd(b);
|
||||
break;case 5:Rd(b);break;case 4:$a(b);break;case 10:ee(b)}a=a.return}Y=null;H=0;La=-1;je=!1;x=null}function ni(){for(;null!==p;){var a=p.effectTag;a&16&&Ab(p.stateNode,"");if(a&128){var b=p.alternate;null!==b&&(b=b.ref,null!==b&&("function"===typeof b?b(null):b.current=null))}switch(a&14){case 2:Gg(p);p.effectTag&=-3;break;case 6:Gg(p);p.effectTag&=-3;Hg(p.alternate,p);break;case 4:Hg(p.alternate,p);break;case 8:a=p,Eg(a),a.return=null,a.child=null,a.memoizedState=null,a.updateQueue=null,a=a.alternate,
|
||||
null!==a&&(a.return=null,a.child=null,a.memoizedState=null,a.updateQueue=null)}p=p.nextEffect}}function oi(){for(;null!==p;){if(p.effectTag&256)a:{var a=p.alternate,b=p;switch(b.tag){case 0:case 11:case 15:Qb(pi,db,b);break a;case 1:if(b.effectTag&256&&null!==a){var c=a.memoizedProps,d=a.memoizedState;a=b.stateNode;b=a.getSnapshotBeforeUpdate(b.elementType===b.type?c:P(b.type,c),d);a.__reactInternalSnapshotBeforeUpdate=b}break a;case 3:case 5:case 6:case 4:case 17:break a;default:n("163")}}p=p.nextEffect}}
|
||||
function qi(a,b){for(;null!==p;){var c=p.effectTag;if(c&36){var d=p.alternate,e=p,f=b;switch(e.tag){case 0:case 11:case 15:Qb(ri,Sb,e);break;case 1:var g=e.stateNode;if(e.effectTag&4)if(null===d)g.componentDidMount();else{var h=e.elementType===e.type?d.memoizedProps:P(e.type,d.memoizedProps);g.componentDidUpdate(h,d.memoizedState,g.__reactInternalSnapshotBeforeUpdate)}d=e.updateQueue;null!==d&&zg(e,d,g,f);break;case 3:d=e.updateQueue;if(null!==d){g=null;if(null!==e.child)switch(e.child.tag){case 5:g=
|
||||
e.child.stateNode;break;case 1:g=e.child.stateNode}zg(e,d,g,f)}break;case 5:f=e.stateNode;null===d&&e.effectTag&4&&Nf(e.type,e.memoizedProps)&&f.focus();break;case 6:break;case 4:break;case 12:break;case 13:break;case 17:break;default:n("163")}}c&128&&(e=p.ref,null!==e&&(f=p.stateNode,"function"===typeof e?e(f):e.current=f));c&512&&(ke=a);p=p.nextEffect}}function si(a,b){Ic=Jc=ke=null;var c=w;w=!0;do{if(b.effectTag&512){var d=!1,e=void 0;try{var f=b;Qb(le,db,f);Qb(db,me,f)}catch(g){d=!0,e=g}d&&Ka(b,
|
||||
e)}b=b.nextEffect}while(null!==b);w=c;c=a.expirationTime;0!==c&&Kc(a,c);z||w||Z(1073741823,!1)}function eb(){null!==Jc&&ti(Jc);null!==Ic&&Ic()}function ui(a,b){Lc=Ca=!0;a.current===b?n("177"):void 0;var c=a.pendingCommitExpirationTime;0===c?n("261"):void 0;a.pendingCommitExpirationTime=0;var d=b.expirationTime,e=b.childExpirationTime;di(a,e>d?e:d);Kg.current=null;d=void 0;1<b.effectTag?null!==b.lastEffect?(b.lastEffect.nextEffect=b,d=b.firstEffect):d=b:d=b.firstEffect;ne=qc;oe=Wh();qc=!1;for(p=d;null!==
|
||||
p;){e=!1;var f=void 0;try{oi()}catch(h){e=!0,f=h}e&&(null===p?n("178"):void 0,Ka(p,f),null!==p&&(p=p.nextEffect))}for(p=d;null!==p;){e=!1;f=void 0;try{ni()}catch(h){e=!0,f=h}e&&(null===p?n("178"):void 0,Ka(p,f),null!==p&&(p=p.nextEffect))}Xh(oe);oe=null;qc=!!ne;ne=null;a.current=b;for(p=d;null!==p;){e=!1;f=void 0;try{qi(a,c)}catch(h){e=!0,f=h}e&&(null===p?n("178"):void 0,Ka(p,f),null!==p&&(p=p.nextEffect))}if(null!==d&&null!==ke){var g=si.bind(null,a,d);Jc=Mc(Lg,function(){return vi(g)});Ic=g}Ca=
|
||||
Lc=!1;"function"===typeof Kd&&Kd(b.stateNode);c=b.expirationTime;b=b.childExpirationTime;b=b>c?b:c;0===b&&(Ba=null);wi(a,b)}function Mg(a){for(;;){var b=a.alternate,c=a.return,d=a.sibling;if(0===(a.effectTag&1024)){x=a;a:{var e=b;b=a;var f=H;var g=b.pendingProps;switch(b.tag){case 2:break;case 16:break;case 15:case 0:break;case 1:E(b.type)&&uc(b);break;case 3:$a(b);Jd(b);g=b.stateNode;g.pendingContext&&(g.context=g.pendingContext,g.pendingContext=null);if(null===e||null===e.child)Zd(b),b.effectTag&=
|
||||
-3;pe(b);break;case 5:Rd(b);var h=Ha(Fb.current);f=b.type;if(null!==e&&null!=b.stateNode)Ng(e,b,f,g,h),e.ref!==b.ref&&(b.effectTag|=128);else if(g){var l=Ha(U.current);if(Zd(b)){g=b;e=g.stateNode;var k=g.type,m=g.memoizedProps,p=h;e[ea]=g;e[ec]=m;f=void 0;h=k;switch(h){case "iframe":case "object":r("load",e);break;case "video":case "audio":for(k=0;k<zb.length;k++)r(zb[k],e);break;case "source":r("error",e);break;case "img":case "image":case "link":r("error",e);r("load",e);break;case "form":r("reset",
|
||||
e);r("submit",e);break;case "details":r("toggle",e);break;case "input":lf(e,m);r("invalid",e);ha(p,"onChange");break;case "select":e._wrapperState={wasMultiple:!!m.multiple};r("invalid",e);ha(p,"onChange");break;case "textarea":If(e,m),r("invalid",e),ha(p,"onChange")}Ed(h,m);k=null;for(f in m)m.hasOwnProperty(f)&&(l=m[f],"children"===f?"string"===typeof l?e.textContent!==l&&(k=["children",l]):"number"===typeof l&&e.textContent!==""+l&&(k=["children",""+l]):Oa.hasOwnProperty(f)&&null!=l&&ha(p,f));
|
||||
switch(h){case "input":kc(e);nf(e,m,!0);break;case "textarea":kc(e);f=e.textContent;f===e._wrapperState.initialValue&&(e.value=f);break;case "select":case "option":break;default:"function"===typeof m.onClick&&(e.onclick=tc)}f=k;g.updateQueue=f;g=null!==f?!0:!1;g&&Pb(b)}else{m=b;p=f;e=g;k=9===h.nodeType?h:h.ownerDocument;"http://www.w3.org/1999/xhtml"===l&&(l=Kf(p));"http://www.w3.org/1999/xhtml"===l?"script"===p?(e=k.createElement("div"),e.innerHTML="<script>\x3c/script>",k=e.removeChild(e.firstChild)):
|
||||
"string"===typeof e.is?k=k.createElement(p,{is:e.is}):(k=k.createElement(p),"select"===p&&(p=k,e.multiple?p.multiple=!0:e.size&&(p.size=e.size))):k=k.createElementNS(l,p);e=k;e[ea]=m;e[ec]=g;Og(e,b,!1,!1);m=e;k=f;p=g;var t=h,y=Fd(k,p);switch(k){case "iframe":case "object":r("load",m);h=p;break;case "video":case "audio":for(h=0;h<zb.length;h++)r(zb[h],m);h=p;break;case "source":r("error",m);h=p;break;case "img":case "image":case "link":r("error",m);r("load",m);h=p;break;case "form":r("reset",m);r("submit",
|
||||
m);h=p;break;case "details":r("toggle",m);h=p;break;case "input":lf(m,p);h=sd(m,p);r("invalid",m);ha(t,"onChange");break;case "option":h=Bd(m,p);break;case "select":m._wrapperState={wasMultiple:!!p.multiple};h=B({},p,{value:void 0});r("invalid",m);ha(t,"onChange");break;case "textarea":If(m,p);h=Cd(m,p);r("invalid",m);ha(t,"onChange");break;default:h=p}Ed(k,h);l=void 0;var u=k,w=m,v=h;for(l in v)if(v.hasOwnProperty(l)){var q=v[l];"style"===l?Mf(w,q):"dangerouslySetInnerHTML"===l?(q=q?q.__html:void 0,
|
||||
null!=q&&Of(w,q)):"children"===l?"string"===typeof q?("textarea"!==u||""!==q)&&Ab(w,q):"number"===typeof q&&Ab(w,""+q):"suppressContentEditableWarning"!==l&&"suppressHydrationWarning"!==l&&"autoFocus"!==l&&(Oa.hasOwnProperty(l)?null!=q&&ha(t,l):null!=q&&rd(w,l,q,y))}switch(k){case "input":kc(m);nf(m,p,!1);break;case "textarea":kc(m);h=m.textContent;h===m._wrapperState.initialValue&&(m.value=h);break;case "option":null!=p.value&&m.setAttribute("value",""+ua(p.value));break;case "select":h=m;m=p;h.multiple=
|
||||
!!m.multiple;p=m.value;null!=p?Xa(h,!!m.multiple,p,!1):null!=m.defaultValue&&Xa(h,!!m.multiple,m.defaultValue,!0);break;default:"function"===typeof h.onClick&&(m.onclick=tc)}(g=Nf(f,g))&&Pb(b);b.stateNode=e}null!==b.ref&&(b.effectTag|=128)}else null===b.stateNode?n("166"):void 0;break;case 6:e&&null!=b.stateNode?Pg(e,b,e.memoizedProps,g):("string"!==typeof g&&(null===b.stateNode?n("166"):void 0),e=Ha(Fb.current),Ha(U.current),Zd(b)?(g=b,f=g.stateNode,e=g.memoizedProps,f[ea]=g,(g=f.nodeValue!==e)&&
|
||||
Pb(b)):(f=b,g=(9===e.nodeType?e:e.ownerDocument).createTextNode(g),g[ea]=b,f.stateNode=g));break;case 11:break;case 13:g=b.memoizedState;if(0!==(b.effectTag&64)){b.expirationTime=f;x=b;break a}g=null!==g;f=null!==e&&null!==e.memoizedState;null!==e&&!g&&f&&(e=e.child.sibling,null!==e&&(h=b.firstEffect,null!==h?(b.firstEffect=e,e.nextEffect=h):(b.firstEffect=b.lastEffect=e,e.nextEffect=null),e.effectTag=8));if(g||f)b.effectTag|=4;break;case 7:break;case 8:break;case 12:break;case 4:$a(b);pe(b);break;
|
||||
case 10:ee(b);break;case 9:break;case 14:break;case 17:E(b.type)&&uc(b);break;case 18:break;default:n("156")}x=null}b=a;if(1===H||1!==b.childExpirationTime){g=0;for(f=b.child;null!==f;)e=f.expirationTime,h=f.childExpirationTime,e>g&&(g=e),h>g&&(g=h),f=f.sibling;b.childExpirationTime=g}if(null!==x)return x;null!==c&&0===(c.effectTag&1024)&&(null===c.firstEffect&&(c.firstEffect=a.firstEffect),null!==a.lastEffect&&(null!==c.lastEffect&&(c.lastEffect.nextEffect=a.firstEffect),c.lastEffect=a.lastEffect),
|
||||
1<a.effectTag&&(null!==c.lastEffect?c.lastEffect.nextEffect=a:c.firstEffect=a,c.lastEffect=a))}else{a=mi(a,H);if(null!==a)return a.effectTag&=1023,a;null!==c&&(c.firstEffect=c.lastEffect=null,c.effectTag|=1024)}if(null!==d)return d;if(null!==c)a=c;else break}return null}function Qg(a){var b=hi(a.alternate,a,H);a.memoizedProps=a.pendingProps;null===b&&(b=Mg(a));Kg.current=null;return b}function Rg(a,b){Ca?n("243"):void 0;eb();Ca=!0;var c=qe.current;qe.current=Ud;var d=a.nextExpirationTimeToWorkOn;
|
||||
if(d!==H||a!==Y||null===x)Jg(),Y=a,H=d,x=Ga(Y.current,null,H),a.pendingCommitExpirationTime=0;var e=!1;do{try{if(b)for(;null!==x&&!Nc();)x=Qg(x);else for(;null!==x;)x=Qg(x)}catch(v){if(Ob=Ja=Nb=null,Vd(),null===x)e=!0,ie(v);else{null===x?n("271"):void 0;var f=x,g=f.return;if(null===g)e=!0,ie(v);else{a:{var h=a,l=g,k=f,m=v;g=H;k.effectTag|=1024;k.firstEffect=k.lastEffect=null;if(null!==m&&"object"===typeof m&&"function"===typeof m.then){var p=m;m=l;var t=-1,r=-1;do{if(13===m.tag){var u=m.alternate;
|
||||
if(null!==u&&(u=u.memoizedState,null!==u)){r=10*(1073741822-u.timedOutAt);break}u=m.pendingProps.maxDuration;if("number"===typeof u)if(0>=u)t=0;else if(-1===t||u<t)t=u}m=m.return}while(null!==m);m=l;do{if(u=13===m.tag)u=void 0===m.memoizedProps.fallback?!1:null===m.memoizedState;if(u){l=m.updateQueue;null===l?(l=new Set,l.add(p),m.updateQueue=l):l.add(p);if(0===(m.mode&1)){m.effectTag|=64;k.effectTag&=-1957;1===k.tag&&(null===k.alternate?k.tag=17:(g=Aa(1073741823),g.tag=Ec,na(k,g)));k.expirationTime=
|
||||
1073741823;break a}k=h;l=g;var w=k.pingCache;null===w?(w=k.pingCache=new xi,u=new Set,w.set(p,u)):(u=w.get(p),void 0===u&&(u=new Set,w.set(p,u)));u.has(l)||(u.add(l),k=yi.bind(null,k,p,l),p.then(k,k));-1===t?h=1073741823:(-1===r&&(r=10*(1073741822-Wf(h,g))-5E3),h=r+t);0<=h&&La<h&&(La=h);m.effectTag|=2048;m.expirationTime=g;break a}m=m.return}while(null!==m);m=Error((sa(k.type)||"A React component")+" suspended while rendering, but no fallback UI was specified.\n\nAdd a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display."+
|
||||
qd(k))}je=!0;m=Hc(m,k);h=l;do{switch(h.tag){case 3:h.effectTag|=2048;h.expirationTime=g;g=he(h,m,g);vg(h,g);break a;case 1:if(t=m,r=h.type,k=h.stateNode,0===(h.effectTag&64)&&("function"===typeof r.getDerivedStateFromError||null!==k&&"function"===typeof k.componentDidCatch&&(null===Ba||!Ba.has(k)))){h.effectTag|=2048;h.expirationTime=g;g=Ig(h,t,g);vg(h,g);break a}}h=h.return}while(null!==h)}x=Mg(f);continue}}}break}while(1);Ca=!1;qe.current=c;Ob=Ja=Nb=null;Vd();if(e)Y=null,a.finishedWork=null;else if(null!==
|
||||
x)a.finishedWork=null;else{c=a.current.alternate;null===c?n("281"):void 0;Y=null;if(je){e=a.latestPendingTime;f=a.latestSuspendedTime;g=a.latestPingedTime;if(0!==e&&e<d||0!==f&&f<d||0!==g&&g<d){Vf(a,d);re(a,c,d,a.expirationTime,-1);return}if(!a.didError&&b){a.didError=!0;d=a.nextExpirationTimeToWorkOn=d;b=a.expirationTime=1073741823;re(a,c,d,b,-1);return}}b&&-1!==La?(Vf(a,d),b=10*(1073741822-Wf(a,d)),b<La&&(La=b),b=10*(1073741822-ka()),b=La-b,re(a,c,d,a.expirationTime,0>b?0:b)):(a.pendingCommitExpirationTime=
|
||||
d,a.finishedWork=c)}}function Ka(a,b){for(var c=a.return;null!==c;){switch(c.tag){case 1:var d=c.stateNode;if("function"===typeof c.type.getDerivedStateFromError||"function"===typeof d.componentDidCatch&&(null===Ba||!Ba.has(d))){a=Hc(b,a);a=Ig(c,a,1073741823);na(c,a);ya(c,1073741823);return}break;case 3:a=Hc(b,a);a=he(c,a,1073741823);na(c,a);ya(c,1073741823);return}c=c.return}3===a.tag&&(c=Hc(b,a),c=he(a,c,1073741823),na(a,c),ya(a,1073741823))}function fb(a,b){var c=zi(),d=void 0;if(0===(b.mode&1))d=
|
||||
1073741823;else if(Ca&&!Lc)d=H;else{switch(c){case se:d=1073741823;break;case te:d=1073741822-10*(((1073741822-a+15)/10|0)+1);break;case Lg:d=1073741822-25*(((1073741822-a+500)/25|0)+1);break;case Ai:case Bi:d=1;break;default:n("313")}null!==Y&&d===H&&--d}c===te&&(0===oa||d<oa)&&(oa=d);return d}function yi(a,b,c){var d=a.pingCache;null!==d&&d.delete(b);if(null!==Y&&H===c)Y=null;else if(b=a.earliestSuspendedTime,d=a.latestSuspendedTime,0!==b&&c<=b&&c>=d){a.didError=!1;b=a.latestPingedTime;if(0===b||
|
||||
b>c)a.latestPingedTime=c;xc(c,a);c=a.expirationTime;0!==c&&Kc(a,c)}}function li(a,b){var c=a.stateNode;null!==c&&c.delete(b);b=ka();b=fb(b,a);a=Sg(a,b);null!==a&&(Bb(a,b),b=a.expirationTime,0!==b&&Kc(a,b))}function Sg(a,b){a.expirationTime<b&&(a.expirationTime=b);var c=a.alternate;null!==c&&c.expirationTime<b&&(c.expirationTime=b);var d=a.return,e=null;if(null===d&&3===a.tag)e=a.stateNode;else for(;null!==d;){c=d.alternate;d.childExpirationTime<b&&(d.childExpirationTime=b);null!==c&&c.childExpirationTime<
|
||||
b&&(c.childExpirationTime=b);if(null===d.return&&3===d.tag){e=d.stateNode;break}d=d.return}return e}function ya(a,b){a=Sg(a,b);null!==a&&(!Ca&&0!==H&&b>H&&Jg(),Bb(a,b),Ca&&!Lc&&Y===a||Kc(a,a.expirationTime),Tb>Ci&&(Tb=0,n("185")))}function Tg(a,b,c,d,e){return Mc(se,function(){return a(b,c,d,e)})}function Ub(){aa=1073741822-((ue()-ve)/10|0)}function Ug(a,b){if(0!==Oc){if(b<Oc)return;null!==Pc&&Vg(Pc)}Oc=b;a=ue()-ve;Pc=Wg(Di,{timeout:10*(1073741822-b)-a})}function re(a,b,c,d,e){a.expirationTime=d;
|
||||
0!==e||Nc()?0<e&&(a.timeoutHandle=Ei(Fi.bind(null,a,b,c),e)):(a.pendingCommitExpirationTime=c,a.finishedWork=b)}function Fi(a,b,c){a.pendingCommitExpirationTime=c;a.finishedWork=b;Ub();jb=aa;Xg(a,c)}function wi(a,b){a.expirationTime=b;a.finishedWork=null}function ka(){if(w)return jb;Qc();if(0===C||1===C)Ub(),jb=aa;return jb}function Kc(a,b){null===a.nextScheduledRoot?(a.expirationTime=b,null===I?(ba=I=a,a.nextScheduledRoot=a):(I=I.nextScheduledRoot=a,I.nextScheduledRoot=ba)):b>a.expirationTime&&(a.expirationTime=
|
||||
b);w||(z?Rc&&(ca=a,C=1073741823,Sc(a,1073741823,!1)):1073741823===b?Z(1073741823,!1):Ug(a,b))}function Qc(){var a=0,b=null;if(null!==I)for(var c=I,d=ba;null!==d;){var e=d.expirationTime;if(0===e){null===c||null===I?n("244"):void 0;if(d===d.nextScheduledRoot){ba=I=d.nextScheduledRoot=null;break}else if(d===ba)ba=e=d.nextScheduledRoot,I.nextScheduledRoot=e,d.nextScheduledRoot=null;else if(d===I){I=c;I.nextScheduledRoot=ba;d.nextScheduledRoot=null;break}else c.nextScheduledRoot=d.nextScheduledRoot,d.nextScheduledRoot=
|
||||
null;d=c.nextScheduledRoot}else{e>a&&(a=e,b=d);if(d===I)break;if(1073741823===a)break;c=d;d=d.nextScheduledRoot}}ca=b;C=a}function Nc(){return Tc?!0:Gi()?Tc=!0:!1}function Di(){try{if(!Nc()&&null!==ba){Ub();var a=ba;do{var b=a.expirationTime;0!==b&&aa<=b&&(a.nextExpirationTimeToWorkOn=aa);a=a.nextScheduledRoot}while(a!==ba)}Z(0,!0)}finally{Tc=!1}}function Z(a,b){Qc();if(b)for(Ub(),jb=aa;null!==ca&&0!==C&&a<=C&&!(Tc&&aa>C);)Sc(ca,C,aa>C),Qc(),Ub(),jb=aa;else for(;null!==ca&&0!==C&&a<=C;)Sc(ca,C,!1),
|
||||
Qc();b&&(Oc=0,Pc=null);0!==C&&Ug(ca,C);Tb=0;we=null;if(null!==kb)for(a=kb,kb=null,b=0;b<a.length;b++){var c=a[b];try{c._onComplete()}catch(d){lb||(lb=!0,Uc=d)}}if(lb)throw a=Uc,Uc=null,lb=!1,a;}function Xg(a,b){w?n("253"):void 0;ca=a;C=b;Sc(a,b,!1);Z(1073741823,!1)}function Sc(a,b,c){w?n("245"):void 0;w=!0;if(c){var d=a.finishedWork;null!==d?Vc(a,d,b):(a.finishedWork=null,d=a.timeoutHandle,-1!==d&&(a.timeoutHandle=-1,Yg(d)),Rg(a,c),d=a.finishedWork,null!==d&&(Nc()?a.finishedWork=d:Vc(a,d,b)))}else d=
|
||||
a.finishedWork,null!==d?Vc(a,d,b):(a.finishedWork=null,d=a.timeoutHandle,-1!==d&&(a.timeoutHandle=-1,Yg(d)),Rg(a,c),d=a.finishedWork,null!==d&&Vc(a,d,b));w=!1}function Vc(a,b,c){var d=a.firstBatch;if(null!==d&&d._expirationTime>=c&&(null===kb?kb=[d]:kb.push(d),d._defer)){a.finishedWork=b;a.expirationTime=0;return}a.finishedWork=null;a===we?Tb++:(we=a,Tb=0);Mc(se,function(){ui(a,b)})}function ie(a){null===ca?n("246"):void 0;ca.expirationTime=0;lb||(lb=!0,Uc=a)}function Zg(a,b){var c=z;z=!0;try{return a(b)}finally{(z=
|
||||
c)||w||Z(1073741823,!1)}}function $g(a,b){if(z&&!Rc){Rc=!0;try{return a(b)}finally{Rc=!1}}return a(b)}function ah(a,b,c){z||w||0===oa||(Z(oa,!1),oa=0);var d=z;z=!0;try{return Mc(te,function(){return a(b,c)})}finally{(z=d)||w||Z(1073741823,!1)}}function bh(a,b,c,d,e){var f=b.current;a:if(c){c=c._reactInternalFiber;b:{2===wb(c)&&1===c.tag?void 0:n("170");var g=c;do{switch(g.tag){case 3:g=g.stateNode.context;break b;case 1:if(E(g.type)){g=g.stateNode.__reactInternalMemoizedMergedChildContext;break b}}g=
|
||||
g.return}while(null!==g);n("171");g=void 0}if(1===c.tag){var h=c.type;if(E(h)){c=Rf(c,h,g);break a}}c=g}else c=va;null===b.context?b.context=c:b.pendingContext=c;b=e;e=Aa(d);e.payload={element:a};b=void 0===b?null:b;null!==b&&(e.callback=b);eb();na(f,e);ya(f,d);return d}function xe(a,b,c,d){var e=b.current,f=ka();e=fb(f,e);return bh(a,b,c,e,d)}function ye(a){a=a.current;if(!a.child)return null;switch(a.child.tag){case 5:return a.child.stateNode;default:return a.child.stateNode}}function Hi(a,b,c){var d=
|
||||
3<arguments.length&&void 0!==arguments[3]?arguments[3]:null;return{$$typeof:Va,key:null==d?null:""+d,children:a,containerInfo:b,implementation:c}}function Vb(a){var b=1073741822-25*(((1073741822-ka()+500)/25|0)+1);b>=ze&&(b=ze-1);this._expirationTime=ze=b;this._root=a;this._callbacks=this._next=null;this._hasChildren=this._didComplete=!1;this._children=null;this._defer=!0}function mb(){this._callbacks=null;this._didCommit=!1;this._onCommit=this._onCommit.bind(this)}function nb(a,b,c){b=S(3,null,null,
|
||||
b?3:0);a={current:b,containerInfo:a,pendingChildren:null,pingCache:null,earliestPendingTime:0,latestPendingTime:0,earliestSuspendedTime:0,latestSuspendedTime:0,latestPingedTime:0,didError:!1,pendingCommitExpirationTime:0,finishedWork:null,timeoutHandle:-1,context:null,pendingContext:null,hydrate:c,nextExpirationTimeToWorkOn:0,expirationTime:0,firstBatch:null,nextScheduledRoot:null};this._internalRoot=b.stateNode=a}function ob(a){return!(!a||1!==a.nodeType&&9!==a.nodeType&&11!==a.nodeType&&(8!==a.nodeType||
|
||||
" react-mount-point-unstable "!==a.nodeValue))}function Ii(a,b){b||(b=a?9===a.nodeType?a.documentElement:a.firstChild:null,b=!(!b||1!==b.nodeType||!b.hasAttribute("data-reactroot")));if(!b)for(var c;c=a.lastChild;)a.removeChild(c);return new nb(a,!1,b)}function Wc(a,b,c,d,e){var f=c._reactRootContainer;if(f){if("function"===typeof e){var g=e;e=function(){var a=ye(f._internalRoot);g.call(a)}}null!=a?f.legacy_renderSubtreeIntoContainer(a,b,e):f.render(b,e)}else{f=c._reactRootContainer=Ii(c,d);if("function"===
|
||||
typeof e){var h=e;e=function(){var a=ye(f._internalRoot);h.call(a)}}$g(function(){null!=a?f.legacy_renderSubtreeIntoContainer(a,b,e):f.render(b,e)})}return ye(f._internalRoot)}function ch(a,b){var c=2<arguments.length&&void 0!==arguments[2]?arguments[2]:null;ob(b)?void 0:n("200");return Hi(a,b,null,c)}da?void 0:n("227");var sh=function(a,b,c,d,e,f,g,h,l){var k=Array.prototype.slice.call(arguments,3);try{b.apply(c,k)}catch(m){this.onError(m)}},qb=!1,$b=null,ac=!1,Yc=null,th={onError:function(a){qb=
|
||||
!0;$b=a}},bc=null,Na={},cc=[],Zc={},Oa={},$c={},bd=null,Ue=null,He=null,rb=null,vh=function(a){if(a){var b=a._dispatchListeners,c=a._dispatchInstances;if(Array.isArray(b))for(var d=0;d<b.length&&!a.isPropagationStopped();d++)Ge(a,b[d],c[d]);else b&&Ge(a,b,c);a._dispatchListeners=null;a._dispatchInstances=null;a.isPersistent()||a.constructor.release(a)}},Ae={injectEventPluginOrder:function(a){bc?n("101"):void 0;bc=Array.prototype.slice.call(a);Ee()},injectEventPluginsByName:function(a){var b=!1,c;
|
||||
for(c in a)if(a.hasOwnProperty(c)){var d=a[c];Na.hasOwnProperty(c)&&Na[c]===d||(Na[c]?n("102",c):void 0,Na[c]=d,b=!0)}b&&Ee()}},dh=Math.random().toString(36).slice(2),ea="__reactInternalInstance$"+dh,ec="__reactEventHandlers$"+dh,ra=!("undefined"===typeof window||!window.document||!window.document.createElement),Ra={animationend:fc("Animation","AnimationEnd"),animationiteration:fc("Animation","AnimationIteration"),animationstart:fc("Animation","AnimationStart"),transitionend:fc("Transition","TransitionEnd")},
|
||||
fd={},Le={};ra&&(Le=document.createElement("div").style,"AnimationEvent"in window||(delete Ra.animationend.animation,delete Ra.animationiteration.animation,delete Ra.animationstart.animation),"TransitionEvent"in window||delete Ra.transitionend.transition);var eh=gc("animationend"),fh=gc("animationiteration"),gh=gc("animationstart"),hh=gc("transitionend"),zb="abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange seeked seeking stalled suspend timeupdate volumechange waiting".split(" "),
|
||||
qa=null,gd=null,hc=null,B=da.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.assign;B(J.prototype,{preventDefault:function(){this.defaultPrevented=!0;var a=this.nativeEvent;a&&(a.preventDefault?a.preventDefault():"unknown"!==typeof a.returnValue&&(a.returnValue=!1),this.isDefaultPrevented=ic)},stopPropagation:function(){var a=this.nativeEvent;a&&(a.stopPropagation?a.stopPropagation():"unknown"!==typeof a.cancelBubble&&(a.cancelBubble=!0),this.isPropagationStopped=ic)},persist:function(){this.isPersistent=
|
||||
ic},isPersistent:jc,destructor:function(){var a=this.constructor.Interface,b;for(b in a)this[b]=null;this.nativeEvent=this._targetInst=this.dispatchConfig=null;this.isPropagationStopped=this.isDefaultPrevented=jc;this._dispatchInstances=this._dispatchListeners=null}});J.Interface={type:null,target:null,currentTarget:function(){return null},eventPhase:null,bubbles:null,cancelable:null,timeStamp:function(a){return a.timeStamp||Date.now()},defaultPrevented:null,isTrusted:null};J.extend=function(a){function b(){return c.apply(this,
|
||||
arguments)}var c=this,d=function(){};d.prototype=c.prototype;d=new d;B(d,b.prototype);b.prototype=d;b.prototype.constructor=b;b.Interface=B({},c.Interface,a);b.extend=c.extend;Ne(b);return b};Ne(J);var Ji=J.extend({data:null}),Ki=J.extend({data:null}),Ah=[9,13,27,32],hd=ra&&"CompositionEvent"in window,Wb=null;ra&&"documentMode"in document&&(Wb=document.documentMode);var Li=ra&&"TextEvent"in window&&!Wb,Se=ra&&(!hd||Wb&&8<Wb&&11>=Wb),Re=String.fromCharCode(32),pa={beforeInput:{phasedRegistrationNames:{bubbled:"onBeforeInput",
|
||||
captured:"onBeforeInputCapture"},dependencies:["compositionend","keypress","textInput","paste"]},compositionEnd:{phasedRegistrationNames:{bubbled:"onCompositionEnd",captured:"onCompositionEndCapture"},dependencies:"blur compositionend keydown keypress keyup mousedown".split(" ")},compositionStart:{phasedRegistrationNames:{bubbled:"onCompositionStart",captured:"onCompositionStartCapture"},dependencies:"blur compositionstart keydown keypress keyup mousedown".split(" ")},compositionUpdate:{phasedRegistrationNames:{bubbled:"onCompositionUpdate",
|
||||
captured:"onCompositionUpdateCapture"},dependencies:"blur compositionupdate keydown keypress keyup mousedown".split(" ")}},Qe=!1,Sa=!1,Mi={eventTypes:pa,extractEvents:function(a,b,c,d){var e=void 0;var f=void 0;if(hd)b:{switch(a){case "compositionstart":e=pa.compositionStart;break b;case "compositionend":e=pa.compositionEnd;break b;case "compositionupdate":e=pa.compositionUpdate;break b}e=void 0}else Sa?Oe(a,c)&&(e=pa.compositionEnd):"keydown"===a&&229===c.keyCode&&(e=pa.compositionStart);e?(Se&&
|
||||
"ko"!==c.locale&&(Sa||e!==pa.compositionStart?e===pa.compositionEnd&&Sa&&(f=Me()):(qa=d,gd="value"in qa?qa.value:qa.textContent,Sa=!0)),e=Ji.getPooled(e,b,c,d),f?e.data=f:(f=Pe(c),null!==f&&(e.data=f)),Qa(e),f=e):f=null;(a=Li?Bh(a,c):Ch(a,c))?(b=Ki.getPooled(pa.beforeInput,b,c,d),b.data=a,Qa(b)):b=null;return null===f?b:null===b?f:[f,b]}},id=null,Ta=null,Ua=null,Ye=function(a,b){return a(b)},yf=function(a,b,c){return a(b,c)},Ze=function(){},jd=!1,Dh={color:!0,date:!0,datetime:!0,"datetime-local":!0,
|
||||
email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0},Ma=da.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;Ma.hasOwnProperty("ReactCurrentDispatcher")||(Ma.ReactCurrentDispatcher={current:null});var Fh=/^(.*)[\\\/]/,O="function"===typeof Symbol&&Symbol.for,Bc=O?Symbol.for("react.element"):60103,Va=O?Symbol.for("react.portal"):60106,ta=O?Symbol.for("react.fragment"):60107,md=O?Symbol.for("react.strict_mode"):60108,lc=O?Symbol.for("react.profiler"):60114,
|
||||
ff=O?Symbol.for("react.provider"):60109,ef=O?Symbol.for("react.context"):60110,ld=O?Symbol.for("react.concurrent_mode"):60111,od=O?Symbol.for("react.forward_ref"):60112,nd=O?Symbol.for("react.suspense"):60113,pd=O?Symbol.for("react.memo"):60115,gf=O?Symbol.for("react.lazy"):60116,df="function"===typeof Symbol&&Symbol.iterator,Hh=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,
|
||||
hf=Object.prototype.hasOwnProperty,kf={},jf={},A={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(a){A[a]=new K(a,0,!1,a,null)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(a){var b=a[0];A[b]=new K(b,1,!1,a[1],null)});["contentEditable","draggable","spellCheck","value"].forEach(function(a){A[a]=new K(a,2,!1,
|
||||
a.toLowerCase(),null)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(a){A[a]=new K(a,2,!1,a,null)});"allowFullScreen async autoFocus autoPlay controls default defer disabled formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(a){A[a]=new K(a,3,!1,a.toLowerCase(),null)});["checked","multiple","muted","selected"].forEach(function(a){A[a]=new K(a,3,!0,a,null)});["capture",
|
||||
"download"].forEach(function(a){A[a]=new K(a,4,!1,a,null)});["cols","rows","size","span"].forEach(function(a){A[a]=new K(a,6,!1,a,null)});["rowSpan","start"].forEach(function(a){A[a]=new K(a,5,!1,a.toLowerCase(),null)});var Be=/[\-:]([a-z])/g,Ce=function(a){return a[1].toUpperCase()};"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(a){var b=
|
||||
a.replace(Be,Ce);A[b]=new K(b,1,!1,a,null)});"xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(a){var b=a.replace(Be,Ce);A[b]=new K(b,1,!1,a,"http://www.w3.org/1999/xlink")});["xml:base","xml:lang","xml:space"].forEach(function(a){var b=a.replace(Be,Ce);A[b]=new K(b,1,!1,a,"http://www.w3.org/XML/1998/namespace")});["tabIndex","crossOrigin"].forEach(function(a){A[a]=new K(a,1,!1,a.toLowerCase(),null)});var pf={change:{phasedRegistrationNames:{bubbled:"onChange",
|
||||
captured:"onChangeCapture"},dependencies:"blur change click focus input keydown keyup selectionchange".split(" ")}},tb=null,ub=null,De=!1;ra&&(De=af("input")&&(!document.documentMode||9<document.documentMode));var Ni={eventTypes:pf,_isInputEventSupported:De,extractEvents:function(a,b,c,d){var e=b?Da(b):window,f=void 0,g=void 0,h=e.nodeName&&e.nodeName.toLowerCase();"select"===h||"input"===h&&"file"===e.type?f=Lh:$e(e)?De?f=Ph:(f=Nh,g=Mh):(h=e.nodeName)&&"input"===h.toLowerCase()&&("checkbox"===e.type||
|
||||
"radio"===e.type)&&(f=Oh);if(f&&(f=f(a,b)))return of(f,c,d);g&&g(a,e,b);"blur"===a&&(a=e._wrapperState)&&a.controlled&&"number"===e.type&&ud(e,"number",e.value)}},Xb=J.extend({view:null,detail:null}),Rh={Alt:"altKey",Control:"ctrlKey",Meta:"metaKey",Shift:"shiftKey"},ih=0,jh=0,kh=!1,lh=!1,Yb=Xb.extend({screenX:null,screenY:null,clientX:null,clientY:null,pageX:null,pageY:null,ctrlKey:null,shiftKey:null,altKey:null,metaKey:null,getModifierState:vd,button:null,buttons:null,relatedTarget:function(a){return a.relatedTarget||
|
||||
(a.fromElement===a.srcElement?a.toElement:a.fromElement)},movementX:function(a){if("movementX"in a)return a.movementX;var b=ih;ih=a.screenX;return kh?"mousemove"===a.type?a.screenX-b:0:(kh=!0,0)},movementY:function(a){if("movementY"in a)return a.movementY;var b=jh;jh=a.screenY;return lh?"mousemove"===a.type?a.screenY-b:0:(lh=!0,0)}}),mh=Yb.extend({pointerId:null,width:null,height:null,pressure:null,tangentialPressure:null,tiltX:null,tiltY:null,twist:null,pointerType:null,isPrimary:null}),Zb={mouseEnter:{registrationName:"onMouseEnter",
|
||||
dependencies:["mouseout","mouseover"]},mouseLeave:{registrationName:"onMouseLeave",dependencies:["mouseout","mouseover"]},pointerEnter:{registrationName:"onPointerEnter",dependencies:["pointerout","pointerover"]},pointerLeave:{registrationName:"onPointerLeave",dependencies:["pointerout","pointerover"]}},Oi={eventTypes:Zb,extractEvents:function(a,b,c,d){var e="mouseover"===a||"pointerover"===a,f="mouseout"===a||"pointerout"===a;if(e&&(c.relatedTarget||c.fromElement)||!f&&!e)return null;e=d.window===
|
||||
d?d:(e=d.ownerDocument)?e.defaultView||e.parentWindow:window;f?(f=b,b=(b=c.relatedTarget||c.toElement)?dc(b):null):f=null;if(f===b)return null;var g=void 0,h=void 0,l=void 0,k=void 0;if("mouseout"===a||"mouseover"===a)g=Yb,h=Zb.mouseLeave,l=Zb.mouseEnter,k="mouse";else if("pointerout"===a||"pointerover"===a)g=mh,h=Zb.pointerLeave,l=Zb.pointerEnter,k="pointer";var m=null==f?e:Da(f);e=null==b?e:Da(b);a=g.getPooled(h,f,c,d);a.type=k+"leave";a.target=m;a.relatedTarget=e;c=g.getPooled(l,b,c,d);c.type=
|
||||
k+"enter";c.target=e;c.relatedTarget=m;d=b;if(f&&d)a:{b=f;e=d;k=0;for(g=b;g;g=fa(g))k++;g=0;for(l=e;l;l=fa(l))g++;for(;0<k-g;)b=fa(b),k--;for(;0<g-k;)e=fa(e),g--;for(;k--;){if(b===e||b===e.alternate)break a;b=fa(b);e=fa(e)}b=null}else b=null;e=b;for(b=[];f&&f!==e;){k=f.alternate;if(null!==k&&k===e)break;b.push(f);f=fa(f)}for(f=[];d&&d!==e;){k=d.alternate;if(null!==k&&k===e)break;f.push(d);d=fa(d)}for(d=0;d<b.length;d++)ed(b[d],"bubbled",a);for(d=f.length;0<d--;)ed(f[d],"captured",c);return[a,c]}},
|
||||
Sh=Object.prototype.hasOwnProperty,Pi=J.extend({animationName:null,elapsedTime:null,pseudoElement:null}),Qi=J.extend({clipboardData:function(a){return"clipboardData"in a?a.clipboardData:window.clipboardData}}),Ri=Xb.extend({relatedTarget:null}),Si={Esc:"Escape",Spacebar:" ",Left:"ArrowLeft",Up:"ArrowUp",Right:"ArrowRight",Down:"ArrowDown",Del:"Delete",Win:"OS",Menu:"ContextMenu",Apps:"ContextMenu",Scroll:"ScrollLock",MozPrintableKey:"Unidentified"},Ti={8:"Backspace",9:"Tab",12:"Clear",13:"Enter",
|
||||
16:"Shift",17:"Control",18:"Alt",19:"Pause",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",45:"Insert",46:"Delete",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"NumLock",145:"ScrollLock",224:"Meta"},Ui=Xb.extend({key:function(a){if(a.key){var b=Si[a.key]||a.key;if("Unidentified"!==b)return b}return"keypress"===a.type?(a=nc(a),13===a?"Enter":
|
||||
String.fromCharCode(a)):"keydown"===a.type||"keyup"===a.type?Ti[a.keyCode]||"Unidentified":""},location:null,ctrlKey:null,shiftKey:null,altKey:null,metaKey:null,repeat:null,locale:null,getModifierState:vd,charCode:function(a){return"keypress"===a.type?nc(a):0},keyCode:function(a){return"keydown"===a.type||"keyup"===a.type?a.keyCode:0},which:function(a){return"keypress"===a.type?nc(a):"keydown"===a.type||"keyup"===a.type?a.keyCode:0}}),Vi=Yb.extend({dataTransfer:null}),Wi=Xb.extend({touches:null,targetTouches:null,
|
||||
changedTouches:null,altKey:null,metaKey:null,ctrlKey:null,shiftKey:null,getModifierState:vd}),Xi=J.extend({propertyName:null,elapsedTime:null,pseudoElement:null}),Yi=Yb.extend({deltaX:function(a){return"deltaX"in a?a.deltaX:"wheelDeltaX"in a?-a.wheelDeltaX:0},deltaY:function(a){return"deltaY"in a?a.deltaY:"wheelDeltaY"in a?-a.wheelDeltaY:"wheelDelta"in a?-a.wheelDelta:0},deltaZ:null,deltaMode:null}),Zi=[["abort","abort"],[eh,"animationEnd"],[fh,"animationIteration"],[gh,"animationStart"],["canplay",
|
||||
"canPlay"],["canplaythrough","canPlayThrough"],["drag","drag"],["dragenter","dragEnter"],["dragexit","dragExit"],["dragleave","dragLeave"],["dragover","dragOver"],["durationchange","durationChange"],["emptied","emptied"],["encrypted","encrypted"],["ended","ended"],["error","error"],["gotpointercapture","gotPointerCapture"],["load","load"],["loadeddata","loadedData"],["loadedmetadata","loadedMetadata"],["loadstart","loadStart"],["lostpointercapture","lostPointerCapture"],["mousemove","mouseMove"],
|
||||
["mouseout","mouseOut"],["mouseover","mouseOver"],["playing","playing"],["pointermove","pointerMove"],["pointerout","pointerOut"],["pointerover","pointerOver"],["progress","progress"],["scroll","scroll"],["seeking","seeking"],["stalled","stalled"],["suspend","suspend"],["timeupdate","timeUpdate"],["toggle","toggle"],["touchmove","touchMove"],[hh,"transitionEnd"],["waiting","waiting"],["wheel","wheel"]],vf={},wd={};[["blur","blur"],["cancel","cancel"],["click","click"],["close","close"],["contextmenu",
|
||||
"contextMenu"],["copy","copy"],["cut","cut"],["auxclick","auxClick"],["dblclick","doubleClick"],["dragend","dragEnd"],["dragstart","dragStart"],["drop","drop"],["focus","focus"],["input","input"],["invalid","invalid"],["keydown","keyDown"],["keypress","keyPress"],["keyup","keyUp"],["mousedown","mouseDown"],["mouseup","mouseUp"],["paste","paste"],["pause","pause"],["play","play"],["pointercancel","pointerCancel"],["pointerdown","pointerDown"],["pointerup","pointerUp"],["ratechange","rateChange"],["reset",
|
||||
"reset"],["seeked","seeked"],["submit","submit"],["touchcancel","touchCancel"],["touchend","touchEnd"],["touchstart","touchStart"],["volumechange","volumeChange"]].forEach(function(a){uf(a,!0)});Zi.forEach(function(a){uf(a,!1)});var nh={eventTypes:vf,isInteractiveTopLevelEventType:function(a){a=wd[a];return void 0!==a&&!0===a.isInteractive},extractEvents:function(a,b,c,d){var e=wd[a];if(!e)return null;switch(a){case "keypress":if(0===nc(c))return null;case "keydown":case "keyup":a=Ui;break;case "blur":case "focus":a=
|
||||
Ri;break;case "click":if(2===c.button)return null;case "auxclick":case "dblclick":case "mousedown":case "mousemove":case "mouseup":case "mouseout":case "mouseover":case "contextmenu":a=Yb;break;case "drag":case "dragend":case "dragenter":case "dragexit":case "dragleave":case "dragover":case "dragstart":case "drop":a=Vi;break;case "touchcancel":case "touchend":case "touchmove":case "touchstart":a=Wi;break;case eh:case fh:case gh:a=Pi;break;case hh:a=Xi;break;case "scroll":a=Xb;break;case "wheel":a=
|
||||
Yi;break;case "copy":case "cut":case "paste":a=Qi;break;case "gotpointercapture":case "lostpointercapture":case "pointercancel":case "pointerdown":case "pointermove":case "pointerout":case "pointerover":case "pointerup":a=mh;break;default:a=J}b=a.getPooled(e,b,c,d);Qa(b);return b}},wf=nh.isInteractiveTopLevelEventType,rc=[],qc=!0,Af={},Vh=0,sc="_reactListenersID"+(""+Math.random()).slice(2),$i=ra&&"documentMode"in document&&11>=document.documentMode,Hf={select:{phasedRegistrationNames:{bubbled:"onSelect",
|
||||
captured:"onSelectCapture"},dependencies:"blur contextmenu dragend focus keydown keyup mousedown mouseup selectionchange".split(" ")}},Wa=null,Ad=null,xb=null,zd=!1,aj={eventTypes:Hf,extractEvents:function(a,b,c,d){var e=d.window===d?d.document:9===d.nodeType?d:d.ownerDocument,f;if(!(f=!e)){a:{e=zf(e);f=$c.onSelect;for(var g=0;g<f.length;g++){var h=f[g];if(!e.hasOwnProperty(h)||!e[h]){e=!1;break a}}e=!0}f=!e}if(f)return null;e=b?Da(b):window;switch(a){case "focus":if($e(e)||"true"===e.contentEditable)Wa=
|
||||
e,Ad=b,xb=null;break;case "blur":xb=Ad=Wa=null;break;case "mousedown":zd=!0;break;case "contextmenu":case "mouseup":case "dragend":return zd=!1,Gf(c,d);case "selectionchange":if($i)break;case "keydown":case "keyup":return Gf(c,d)}return null}};Ae.injectEventPluginOrder("ResponderEventPlugin SimpleEventPlugin EnterLeaveEventPlugin ChangeEventPlugin SelectEventPlugin BeforeInputEventPlugin".split(" "));(function(a,b,c){bd=a;Ue=b;He=c})(dd,Je,Da);Ae.injectEventPluginsByName({SimpleEventPlugin:nh,EnterLeaveEventPlugin:Oi,
|
||||
ChangeEventPlugin:Ni,SelectEventPlugin:aj,BeforeInputEventPlugin:Mi});var Xc=void 0,Of=function(a){return"undefined"!==typeof MSApp&&MSApp.execUnsafeLocalFunction?function(b,c,d,e){MSApp.execUnsafeLocalFunction(function(){return a(b,c,d,e)})}:a}(function(a,b){if("http://www.w3.org/2000/svg"!==a.namespaceURI||"innerHTML"in a)a.innerHTML=b;else{Xc=Xc||document.createElement("div");Xc.innerHTML="<svg>"+b+"</svg>";for(b=Xc.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;b.firstChild;)a.appendChild(b.firstChild)}}),
|
||||
Ab=function(a,b){if(b){var c=a.firstChild;if(c&&c===a.lastChild&&3===c.nodeType){c.nodeValue=b;return}}a.textContent=b},yb={animationIterationCount:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,
|
||||
lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},bj=["Webkit","ms","Moz","O"];Object.keys(yb).forEach(function(a){bj.forEach(function(b){b=b+a.charAt(0).toUpperCase()+a.substring(1);yb[b]=yb[a]})});var Zh=B({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,
|
||||
source:!0,track:!0,wbr:!0}),R=da.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler,Vg=R.unstable_cancelCallback,ue=R.unstable_now,Wg=R.unstable_scheduleCallback,Gi=R.unstable_shouldYield,Mc=R.unstable_runWithPriority,zi=R.unstable_getCurrentPriorityLevel,se=R.unstable_ImmediatePriority,te=R.unstable_UserBlockingPriority,Lg=R.unstable_NormalPriority,Ai=R.unstable_LowPriority,Bi=R.unstable_IdlePriority,ne=null,oe=null,Ei="function"===typeof setTimeout?setTimeout:void 0,Yg="function"===typeof clearTimeout?
|
||||
clearTimeout:void 0,vi=Wg,ti=Vg;new Set;var Id=[],Ya=-1,va={},F={current:va},M={current:!1},Fa=va,Kd=null,Ld=null,S=function(a,b,c,d){return new bi(a,b,c,d)},$f=(new da.Component).refs,zc={isMounted:function(a){return(a=a._reactInternalFiber)?2===wb(a):!1},enqueueSetState:function(a,b,c){a=a._reactInternalFiber;var d=ka();d=fb(d,a);var e=Aa(d);e.payload=b;void 0!==c&&null!==c&&(e.callback=c);eb();na(a,e);ya(a,d)},enqueueReplaceState:function(a,b,c){a=a._reactInternalFiber;var d=ka();d=fb(d,a);var e=
|
||||
Aa(d);e.tag=yg;e.payload=b;void 0!==c&&null!==c&&(e.callback=c);eb();na(a,e);ya(a,d)},enqueueForceUpdate:function(a,b){a=a._reactInternalFiber;var c=ka();c=fb(c,a);var d=Aa(c);d.tag=Ec;void 0!==b&&null!==b&&(d.callback=b);eb();na(a,d);ya(a,c)}},Cc=Array.isArray,hb=ag(!0),ae=ag(!1),Eb={},U={current:Eb},Gb={current:Eb},Fb={current:Eb},db=0,pi=2,Rb=4,ji=8,ri=16,Sb=32,me=64,le=128,Dc=Ma.ReactCurrentDispatcher,Hb=0,xa=null,y=null,W=null,bb=null,G=null,ab=null,Kb=0,X=null,Lb=0,Ib=!1,ia=null,Jb=0,Ud={readContext:T,
|
||||
useCallback:V,useContext:V,useEffect:V,useImperativeHandle:V,useLayoutEffect:V,useMemo:V,useReducer:V,useRef:V,useState:V,useDebugValue:V},fi={readContext:T,useCallback:function(a,b){cb().memoizedState=[a,void 0===b?null:b];return a},useContext:T,useEffect:function(a,b){return Xd(516,le|me,a,b)},useImperativeHandle:function(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;return Xd(4,Rb|Sb,fg.bind(null,b,a),c)},useLayoutEffect:function(a,b){return Xd(4,Rb|Sb,a,b)},useMemo:function(a,b){var c=cb();
|
||||
b=void 0===b?null:b;a=a();c.memoizedState=[a,b];return a},useReducer:function(a,b,c){var d=cb();b=void 0!==c?c(b):b;d.memoizedState=d.baseState=b;a=d.queue={last:null,dispatch:null,lastRenderedReducer:a,lastRenderedState:b};a=a.dispatch=hg.bind(null,xa,a);return[d.memoizedState,a]},useRef:function(a){var b=cb();a={current:a};return b.memoizedState=a},useState:function(a){var b=cb();"function"===typeof a&&(a=a());b.memoizedState=b.baseState=a;a=b.queue={last:null,dispatch:null,lastRenderedReducer:dg,
|
||||
lastRenderedState:a};a=a.dispatch=hg.bind(null,xa,a);return[b.memoizedState,a]},useDebugValue:gg},cg={readContext:T,useCallback:function(a,b){var c=Mb();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&Sd(b,d[1]))return d[0];c.memoizedState=[a,b];return a},useContext:T,useEffect:function(a,b){return Yd(516,le|me,a,b)},useImperativeHandle:function(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;return Yd(4,Rb|Sb,fg.bind(null,b,a),c)},useLayoutEffect:function(a,b){return Yd(4,Rb|Sb,
|
||||
a,b)},useMemo:function(a,b){var c=Mb();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&Sd(b,d[1]))return d[0];a=a();c.memoizedState=[a,b];return a},useReducer:eg,useRef:function(a){return Mb().memoizedState},useState:function(a){return eg(dg,a)},useDebugValue:gg},la=null,gb=null,Ia=!1,gi=Ma.ReactCurrentOwner,ja=!1,de={current:null},Nb=null,Ja=null,Ob=null,ug=0,yg=1,Ec=2,ge=3,za=!1,Og=void 0,pe=void 0,Ng=void 0,Pg=void 0;Og=function(a,b,c,d){for(c=b.child;null!==c;){if(5===c.tag||
|
||||
6===c.tag)a.appendChild(c.stateNode);else if(4!==c.tag&&null!==c.child){c.child.return=c;c=c.child;continue}if(c===b)break;for(;null===c.sibling;){if(null===c.return||c.return===b)return;c=c.return}c.sibling.return=c.return;c=c.sibling}};pe=function(a){};Ng=function(a,b,c,d,e){var f=a.memoizedProps;if(f!==d){var g=b.stateNode;Ha(U.current);a=null;switch(c){case "input":f=sd(g,f);d=sd(g,d);a=[];break;case "option":f=Bd(g,f);d=Bd(g,d);a=[];break;case "select":f=B({},f,{value:void 0});d=B({},d,{value:void 0});
|
||||
a=[];break;case "textarea":f=Cd(g,f);d=Cd(g,d);a=[];break;default:"function"!==typeof f.onClick&&"function"===typeof d.onClick&&(g.onclick=tc)}Ed(c,d);g=c=void 0;var h=null;for(c in f)if(!d.hasOwnProperty(c)&&f.hasOwnProperty(c)&&null!=f[c])if("style"===c){var l=f[c];for(g in l)l.hasOwnProperty(g)&&(h||(h={}),h[g]="")}else"dangerouslySetInnerHTML"!==c&&"children"!==c&&"suppressContentEditableWarning"!==c&&"suppressHydrationWarning"!==c&&"autoFocus"!==c&&(Oa.hasOwnProperty(c)?a||(a=[]):(a=a||[]).push(c,
|
||||
null));for(c in d){var k=d[c];l=null!=f?f[c]:void 0;if(d.hasOwnProperty(c)&&k!==l&&(null!=k||null!=l))if("style"===c)if(l){for(g in l)!l.hasOwnProperty(g)||k&&k.hasOwnProperty(g)||(h||(h={}),h[g]="");for(g in k)k.hasOwnProperty(g)&&l[g]!==k[g]&&(h||(h={}),h[g]=k[g])}else h||(a||(a=[]),a.push(c,h)),h=k;else"dangerouslySetInnerHTML"===c?(k=k?k.__html:void 0,l=l?l.__html:void 0,null!=k&&l!==k&&(a=a||[]).push(c,""+k)):"children"===c?l===k||"string"!==typeof k&&"number"!==typeof k||(a=a||[]).push(c,""+
|
||||
k):"suppressContentEditableWarning"!==c&&"suppressHydrationWarning"!==c&&(Oa.hasOwnProperty(c)?(null!=k&&ha(e,c),a||l===k||(a=[])):(a=a||[]).push(c,k))}h&&(a=a||[]).push("style",h);e=a;(b.updateQueue=e)&&Pb(b)}};Pg=function(a,b,c,d){c!==d&&Pb(b)};var ki="function"===typeof WeakSet?WeakSet:Set,xi="function"===typeof WeakMap?WeakMap:Map,qe=Ma.ReactCurrentDispatcher,Kg=Ma.ReactCurrentOwner,ze=1073741822,Ca=!1,x=null,Y=null,H=0,La=-1,je=!1,p=null,Lc=!1,ke=null,Jc=null,Ic=null,Ba=null,ba=null,I=null,Oc=
|
||||
0,Pc=void 0,w=!1,ca=null,C=0,oa=0,lb=!1,Uc=null,z=!1,Rc=!1,kb=null,ve=ue(),aa=1073741822-(ve/10|0),jb=aa,Ci=50,Tb=0,we=null,Tc=!1;id=function(a,b,c){switch(b){case "input":td(a,c);b=c.name;if("radio"===c.type&&null!=b){for(c=a;c.parentNode;)c=c.parentNode;c=c.querySelectorAll("input[name="+JSON.stringify(""+b)+'][type="radio"]');for(b=0;b<c.length;b++){var d=c[b];if(d!==a&&d.form===a.form){var e=dd(d);e?void 0:n("90");cf(d);td(d,e)}}}break;case "textarea":Jf(a,c);break;case "select":b=c.value,null!=
|
||||
b&&Xa(a,!!c.multiple,b,!1)}};Vb.prototype.render=function(a){this._defer?void 0:n("250");this._hasChildren=!0;this._children=a;var b=this._root._internalRoot,c=this._expirationTime,d=new mb;bh(a,b,null,c,d._onCommit);return d};Vb.prototype.then=function(a){if(this._didComplete)a();else{var b=this._callbacks;null===b&&(b=this._callbacks=[]);b.push(a)}};Vb.prototype.commit=function(){var a=this._root._internalRoot,b=a.firstBatch;this._defer&&null!==b?void 0:n("251");if(this._hasChildren){var c=this._expirationTime;
|
||||
if(b!==this){this._hasChildren&&(c=this._expirationTime=b._expirationTime,this.render(this._children));for(var d=null,e=b;e!==this;)d=e,e=e._next;null===d?n("251"):void 0;d._next=e._next;this._next=b;a.firstBatch=this}this._defer=!1;Xg(a,c);b=this._next;this._next=null;b=a.firstBatch=b;null!==b&&b._hasChildren&&b.render(b._children)}else this._next=null,this._defer=!1};Vb.prototype._onComplete=function(){if(!this._didComplete){this._didComplete=!0;var a=this._callbacks;if(null!==a)for(var b=0;b<a.length;b++)(0,a[b])()}};
|
||||
mb.prototype.then=function(a){if(this._didCommit)a();else{var b=this._callbacks;null===b&&(b=this._callbacks=[]);b.push(a)}};mb.prototype._onCommit=function(){if(!this._didCommit){this._didCommit=!0;var a=this._callbacks;if(null!==a)for(var b=0;b<a.length;b++){var c=a[b];"function"!==typeof c?n("191",c):void 0;c()}}};nb.prototype.render=function(a,b){var c=this._internalRoot,d=new mb;b=void 0===b?null:b;null!==b&&d.then(b);xe(a,c,null,d._onCommit);return d};nb.prototype.unmount=function(a){var b=
|
||||
this._internalRoot,c=new mb;a=void 0===a?null:a;null!==a&&c.then(a);xe(null,b,null,c._onCommit);return c};nb.prototype.legacy_renderSubtreeIntoContainer=function(a,b,c){var d=this._internalRoot,e=new mb;c=void 0===c?null:c;null!==c&&e.then(c);xe(b,d,a,e._onCommit);return e};nb.prototype.createBatch=function(){var a=new Vb(this),b=a._expirationTime,c=this._internalRoot,d=c.firstBatch;if(null===d)c.firstBatch=a,a._next=null;else{for(c=null;null!==d&&d._expirationTime>=b;)c=d,d=d._next;a._next=d;null!==
|
||||
c&&(c._next=a)}return a};(function(a,b,c){Ye=a;yf=b;Ze=c})(Zg,ah,function(){w||0===oa||(Z(oa,!1),oa=0)});var oh={createPortal:ch,findDOMNode:function(a){if(null==a)return null;if(1===a.nodeType)return a;var b=a._reactInternalFiber;void 0===b&&("function"===typeof a.render?n("188"):n("268",Object.keys(a)));a=tf(b);a=null===a?null:a.stateNode;return a},hydrate:function(a,b,c){ob(b)?void 0:n("200");return Wc(null,a,b,!0,c)},render:function(a,b,c){ob(b)?void 0:n("200");return Wc(null,a,b,!1,c)},unstable_renderSubtreeIntoContainer:function(a,
|
||||
b,c,d){ob(c)?void 0:n("200");null==a||void 0===a._reactInternalFiber?n("38"):void 0;return Wc(a,b,c,!1,d)},unmountComponentAtNode:function(a){ob(a)?void 0:n("40");return a._reactRootContainer?($g(function(){Wc(null,null,a,!1,function(){a._reactRootContainer=null})}),!0):!1},unstable_createPortal:function(){return ch.apply(void 0,arguments)},unstable_batchedUpdates:Zg,unstable_interactiveUpdates:ah,flushSync:function(a,b){w?n("187"):void 0;var c=z;z=!0;try{return Tg(a,b)}finally{z=c,Z(1073741823,!1)}},
|
||||
unstable_createRoot:function(a,b){ob(a)?void 0:n("299","unstable_createRoot");return new nb(a,!0,null!=b&&!0===b.hydrate)},unstable_flushControlled:function(a){var b=z;z=!0;try{Tg(a)}finally{(z=b)||w||Z(1073741823,!1)}},__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:{Events:[Je,Da,dd,Ae.injectEventPluginsByName,Zc,Qa,function(a){ad(a,xh)},Ve,We,oc,cd]}};(function(a){var b=a.findFiberByHostInstance;return ai(B({},a,{overrideProps:null,currentDispatcherRef:Ma.ReactCurrentDispatcher,findHostInstanceByFiber:function(a){a=
|
||||
tf(a);return null===a?null:a.stateNode},findFiberByHostInstance:function(a){return b?b(a):null}}))})({findFiberByHostInstance:dc,bundleType:0,version:"16.8.6",rendererPackageName:"react-dom"});var ph={default:oh},qh=ph&&oh||ph;return qh.default||qh});
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/** @license React v16.8.6
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
'use strict';(function(N,q){"object"===typeof exports&&"undefined"!==typeof module?module.exports=q():"function"===typeof define&&define.amd?define(q):N.React=q()})(this,function(){function N(a,b,d,g,p,c,e,h){if(!a){a=void 0;if(void 0===b)a=Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var n=[d,g,p,c,e,h],f=0;a=Error(b.replace(/%s/g,function(){return n[f++]}));a.name="Invariant Violation"}a.framesToPop=1;
|
||||
throw a;}}function q(a){for(var b=arguments.length-1,d="https://reactjs.org/docs/error-decoder.html?invariant="+a,g=0;g<b;g++)d+="&args[]="+encodeURIComponent(arguments[g+1]);N(!1,"Minified React error #"+a+"; visit %s for the full message or use the non-minified dev environment for full errors and additional helpful warnings. ",d)}function t(a,b,d){this.props=a;this.context=b;this.refs=ba;this.updater=d||ca}function da(){}function O(a,b,d){this.props=a;this.context=b;this.refs=ba;this.updater=d||
|
||||
ca}function u(){if(!x){var a=c.expirationTime;C?P():C=!0;D(ta,a)}}function Q(){var a=c,b=c.next;if(c===b)c=null;else{var d=c.previous;c=d.next=b;b.previous=d}a.next=a.previous=null;d=a.callback;b=a.expirationTime;a=a.priorityLevel;var g=f,p=E;f=a;E=b;try{var n=d()}finally{f=g,E=p}if("function"===typeof n)if(n={callback:n,priorityLevel:a,expirationTime:b,next:null,previous:null},null===c)c=n.next=n.previous=n;else{d=null;a=c;do{if(a.expirationTime>=b){d=a;break}a=a.next}while(a!==c);null===d?d=c:d===
|
||||
c&&(c=n,u());b=d.previous;b.next=d.previous=n;n.next=d;n.previous=b}}function F(){if(-1===k&&null!==c&&1===c.priorityLevel){x=!0;try{do Q();while(null!==c&&1===c.priorityLevel)}finally{x=!1,null!==c?u():C=!1}}}function ta(a){x=!0;var b=G;G=a;try{if(a)for(;null!==c;){var d=l();if(c.expirationTime<=d){do Q();while(null!==c&&c.expirationTime<=d)}else break}else if(null!==c){do Q();while(null!==c&&!H())}}finally{x=!1,G=b,null!==c?u():C=!1,F()}}function ea(a,b,d){var g=void 0,p={},c=null,e=null;if(null!=
|
||||
b)for(g in void 0!==b.ref&&(e=b.ref),void 0!==b.key&&(c=""+b.key),b)fa.call(b,g)&&!ha.hasOwnProperty(g)&&(p[g]=b[g]);var h=arguments.length-2;if(1===h)p.children=d;else if(1<h){for(var f=Array(h),k=0;k<h;k++)f[k]=arguments[k+2];p.children=f}if(a&&a.defaultProps)for(g in h=a.defaultProps,h)void 0===p[g]&&(p[g]=h[g]);return{$$typeof:y,type:a,key:c,ref:e,props:p,_owner:R.current}}function ua(a,b){return{$$typeof:y,type:a.type,key:b,ref:a.ref,props:a.props,_owner:a._owner}}function S(a){return"object"===
|
||||
typeof a&&null!==a&&a.$$typeof===y}function va(a){var b={"=":"=0",":":"=2"};return"$"+(""+a).replace(/[=:]/g,function(a){return b[a]})}function ia(a,b,d,g){if(I.length){var c=I.pop();c.result=a;c.keyPrefix=b;c.func=d;c.context=g;c.count=0;return c}return{result:a,keyPrefix:b,func:d,context:g,count:0}}function ja(a){a.result=null;a.keyPrefix=null;a.func=null;a.context=null;a.count=0;10>I.length&&I.push(a)}function T(a,b,d,g){var c=typeof a;if("undefined"===c||"boolean"===c)a=null;var e=!1;if(null===
|
||||
a)e=!0;else switch(c){case "string":case "number":e=!0;break;case "object":switch(a.$$typeof){case y:case wa:e=!0}}if(e)return d(g,a,""===b?"."+U(a,0):b),1;e=0;b=""===b?".":b+":";if(Array.isArray(a))for(var f=0;f<a.length;f++){c=a[f];var h=b+U(c,f);e+=T(c,h,d,g)}else if(null===a||"object"!==typeof a?h=null:(h=ka&&a[ka]||a["@@iterator"],h="function"===typeof h?h:null),"function"===typeof h)for(a=h.call(a),f=0;!(c=a.next()).done;)c=c.value,h=b+U(c,f++),e+=T(c,h,d,g);else"object"===c&&(d=""+a,q("31",
|
||||
"[object Object]"===d?"object with keys {"+Object.keys(a).join(", ")+"}":d,""));return e}function V(a,b,d){return null==a?0:T(a,"",b,d)}function U(a,b){return"object"===typeof a&&null!==a&&null!=a.key?va(a.key):b.toString(36)}function xa(a,b,d){a.func.call(a.context,b,a.count++)}function ya(a,b,d){var g=a.result,c=a.keyPrefix;a=a.func.call(a.context,b,a.count++);Array.isArray(a)?W(a,g,d,function(a){return a}):null!=a&&(S(a)&&(a=ua(a,c+(!a.key||b&&b.key===a.key?"":(""+a.key).replace(la,"$&/")+"/")+
|
||||
d)),g.push(a))}function W(a,b,d,g,c){var e="";null!=d&&(e=(""+d).replace(la,"$&/")+"/");b=ia(b,e,g,c);V(a,ya,b);ja(b)}function m(){var a=ma.current;null===a?q("321"):void 0;return a}var e="function"===typeof Symbol&&Symbol.for,y=e?Symbol.for("react.element"):60103,wa=e?Symbol.for("react.portal"):60106,r=e?Symbol.for("react.fragment"):60107,X=e?Symbol.for("react.strict_mode"):60108,za=e?Symbol.for("react.profiler"):60114,Aa=e?Symbol.for("react.provider"):60109,Ba=e?Symbol.for("react.context"):60110,
|
||||
Ca=e?Symbol.for("react.concurrent_mode"):60111,Da=e?Symbol.for("react.forward_ref"):60112,Ea=e?Symbol.for("react.suspense"):60113,Fa=e?Symbol.for("react.memo"):60115,Ga=e?Symbol.for("react.lazy"):60116,ka="function"===typeof Symbol&&Symbol.iterator,na=Object.getOwnPropertySymbols,Ha=Object.prototype.hasOwnProperty,Ia=Object.prototype.propertyIsEnumerable,J=function(){try{if(!Object.assign)return!1;var a=new String("abc");a[5]="de";if("5"===Object.getOwnPropertyNames(a)[0])return!1;var b={};for(a=
|
||||
0;10>a;a++)b["_"+String.fromCharCode(a)]=a;if("0123456789"!==Object.getOwnPropertyNames(b).map(function(a){return b[a]}).join(""))return!1;var d={};"abcdefghijklmnopqrst".split("").forEach(function(a){d[a]=a});return"abcdefghijklmnopqrst"!==Object.keys(Object.assign({},d)).join("")?!1:!0}catch(g){return!1}}()?Object.assign:function(a,b){if(null===a||void 0===a)throw new TypeError("Object.assign cannot be called with null or undefined");var d=Object(a);for(var c,e=1;e<arguments.length;e++){var f=Object(arguments[e]);
|
||||
for(var k in f)Ha.call(f,k)&&(d[k]=f[k]);if(na){c=na(f);for(var h=0;h<c.length;h++)Ia.call(f,c[h])&&(d[c[h]]=f[c[h]])}}return d},ca={isMounted:function(a){return!1},enqueueForceUpdate:function(a,b,d){},enqueueReplaceState:function(a,b,d,c){},enqueueSetState:function(a,b,d,c){}},ba={};t.prototype.isReactComponent={};t.prototype.setState=function(a,b){"object"!==typeof a&&"function"!==typeof a&&null!=a?q("85"):void 0;this.updater.enqueueSetState(this,a,b,"setState")};t.prototype.forceUpdate=function(a){this.updater.enqueueForceUpdate(this,
|
||||
a,"forceUpdate")};da.prototype=t.prototype;e=O.prototype=new da;e.constructor=O;J(e,t.prototype);e.isPureReactComponent=!0;var c=null,G=!1,f=3,k=-1,E=-1,x=!1,C=!1,Ja=Date,Ka="function"===typeof setTimeout?setTimeout:void 0,La="function"===typeof clearTimeout?clearTimeout:void 0,oa="function"===typeof requestAnimationFrame?requestAnimationFrame:void 0,pa="function"===typeof cancelAnimationFrame?cancelAnimationFrame:void 0,qa,ra,Y=function(a){qa=oa(function(b){La(ra);a(b)});ra=Ka(function(){pa(qa);
|
||||
a(l())},100)};if("object"===typeof performance&&"function"===typeof performance.now){var Ma=performance;var l=function(){return Ma.now()}}else l=function(){return Ja.now()};e=null;"undefined"!==typeof window?e=window:"undefined"!==typeof global&&(e=global);if(e&&e._schedMock){e=e._schedMock;var D=e[0];var P=e[1];var H=e[2];l=e[3]}else if("undefined"===typeof window||"function"!==typeof MessageChannel){var v=null,Na=function(a){if(null!==v)try{v(a)}finally{v=null}};D=function(a,b){null!==v?setTimeout(D,
|
||||
0,a):(v=a,setTimeout(Na,0,!1))};P=function(){v=null};H=function(){return!1}}else{"undefined"!==typeof console&&("function"!==typeof oa&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills"),"function"!==typeof pa&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills"));var w=null,K=!1,z=-1,A=!1,Z=!1,L=0,
|
||||
M=33,B=33;H=function(){return L<=l()};e=new MessageChannel;var sa=e.port2;e.port1.onmessage=function(a){K=!1;a=w;var b=z;w=null;z=-1;var d=l(),c=!1;if(0>=L-d)if(-1!==b&&b<=d)c=!0;else{A||(A=!0,Y(aa));w=a;z=b;return}if(null!==a){Z=!0;try{a(c)}finally{Z=!1}}};var aa=function(a){if(null!==w){Y(aa);var b=a-L+B;b<B&&M<B?(8>b&&(b=8),B=b<M?M:b):M=b;L=a+B;K||(K=!0,sa.postMessage(void 0))}else A=!1};D=function(a,b){w=a;z=b;Z||0>b?sa.postMessage(void 0):A||(A=!0,Y(aa))};P=function(){w=null;K=!1;z=-1}}var Oa=
|
||||
0,ma={current:null},R={current:null};e={ReactCurrentDispatcher:ma,ReactCurrentOwner:R,assign:J};J(e,{Scheduler:{unstable_cancelCallback:function(a){var b=a.next;if(null!==b){if(b===a)c=null;else{a===c&&(c=b);var d=a.previous;d.next=b;b.previous=d}a.next=a.previous=null}},unstable_shouldYield:function(){return!G&&(null!==c&&c.expirationTime<E||H())},unstable_now:l,unstable_scheduleCallback:function(a,b){var d=-1!==k?k:l();if("object"===typeof b&&null!==b&&"number"===typeof b.timeout)b=d+b.timeout;
|
||||
else switch(f){case 1:b=d+-1;break;case 2:b=d+250;break;case 5:b=d+1073741823;break;case 4:b=d+1E4;break;default:b=d+5E3}a={callback:a,priorityLevel:f,expirationTime:b,next:null,previous:null};if(null===c)c=a.next=a.previous=a,u();else{d=null;var g=c;do{if(g.expirationTime>b){d=g;break}g=g.next}while(g!==c);null===d?d=c:d===c&&(c=a,u());b=d.previous;b.next=d.previous=a;a.next=d;a.previous=b}return a},unstable_runWithPriority:function(a,b){switch(a){case 1:case 2:case 3:case 4:case 5:break;default:a=
|
||||
3}var d=f,c=k;f=a;k=l();try{return b()}finally{f=d,k=c,F()}},unstable_next:function(a){switch(f){case 1:case 2:case 3:var b=3;break;default:b=f}var d=f,c=k;f=b;k=l();try{return a()}finally{f=d,k=c,F()}},unstable_wrapCallback:function(a){var b=f;return function(){var d=f,c=k;f=b;k=l();try{return a.apply(this,arguments)}finally{f=d,k=c,F()}}},unstable_getFirstCallbackNode:function(){return c},unstable_pauseExecution:function(){},unstable_continueExecution:function(){null!==c&&u()},unstable_getCurrentPriorityLevel:function(){return f},
|
||||
unstable_IdlePriority:5,unstable_ImmediatePriority:1,unstable_LowPriority:4,unstable_NormalPriority:3,unstable_UserBlockingPriority:2},SchedulerTracing:{__interactionsRef:null,__subscriberRef:null,unstable_clear:function(a){return a()},unstable_getCurrent:function(){return null},unstable_getThreadID:function(){return++Oa},unstable_subscribe:function(a){},unstable_trace:function(a,b,d){return d()},unstable_unsubscribe:function(a){},unstable_wrap:function(a){return a}}});var fa=Object.prototype.hasOwnProperty,
|
||||
ha={key:!0,ref:!0,__self:!0,__source:!0},la=/\/+/g,I=[];r={Children:{map:function(a,b,d){if(null==a)return a;var c=[];W(a,c,null,b,d);return c},forEach:function(a,b,d){if(null==a)return a;b=ia(null,null,b,d);V(a,xa,b);ja(b)},count:function(a){return V(a,function(){return null},null)},toArray:function(a){var b=[];W(a,b,null,function(a){return a});return b},only:function(a){S(a)?void 0:q("143");return a}},createRef:function(){return{current:null}},Component:t,PureComponent:O,createContext:function(a,
|
||||
b){void 0===b&&(b=null);a={$$typeof:Ba,_calculateChangedBits:b,_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null};a.Provider={$$typeof:Aa,_context:a};return a.Consumer=a},forwardRef:function(a){return{$$typeof:Da,render:a}},lazy:function(a){return{$$typeof:Ga,_ctor:a,_status:-1,_result:null}},memo:function(a,b){return{$$typeof:Fa,type:a,compare:void 0===b?null:b}},useCallback:function(a,b){return m().useCallback(a,b)},useContext:function(a,b){return m().useContext(a,b)},
|
||||
useEffect:function(a,b){return m().useEffect(a,b)},useImperativeHandle:function(a,b,d){return m().useImperativeHandle(a,b,d)},useDebugValue:function(a,b){},useLayoutEffect:function(a,b){return m().useLayoutEffect(a,b)},useMemo:function(a,b){return m().useMemo(a,b)},useReducer:function(a,b,d){return m().useReducer(a,b,d)},useRef:function(a){return m().useRef(a)},useState:function(a){return m().useState(a)},Fragment:r,StrictMode:X,Suspense:Ea,createElement:ea,cloneElement:function(a,b,d){null===a||
|
||||
void 0===a?q("267",a):void 0;var c=void 0,e=J({},a.props),f=a.key,k=a.ref,h=a._owner;if(null!=b){void 0!==b.ref&&(k=b.ref,h=R.current);void 0!==b.key&&(f=""+b.key);var l=void 0;a.type&&a.type.defaultProps&&(l=a.type.defaultProps);for(c in b)fa.call(b,c)&&!ha.hasOwnProperty(c)&&(e[c]=void 0===b[c]&&void 0!==l?l[c]:b[c])}c=arguments.length-2;if(1===c)e.children=d;else if(1<c){l=Array(c);for(var m=0;m<c;m++)l[m]=arguments[m+2];e.children=l}return{$$typeof:y,type:a.type,key:f,ref:k,props:e,_owner:h}},
|
||||
createFactory:function(a){var b=ea.bind(null,a);b.type=a;return b},isValidElement:S,version:"16.8.6",unstable_ConcurrentMode:Ca,unstable_Profiler:za,__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:e};r=(X={default:r},r)||X;return r.default||r});
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
|
||||
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
.main {
|
||||
height: 100%;
|
||||
|
||||
font-size: 14px;
|
||||
display: grid;
|
||||
grid-template-columns: 640px 1fr;
|
||||
}
|
||||
|
||||
.left-thing {
|
||||
background-color: gray;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.left-thing hr {
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 10px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.panel button {
|
||||
flex: 0 0 32%;
|
||||
height: 32px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
margin-left: 3px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 10px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.flags {
|
||||
padding: 10px;
|
||||
}
|
||||
.log {
|
||||
flex: 1 1 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.clear-log {
|
||||
font-size: 14px;
|
||||
color: #eeeeee;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.right-thing {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Message widget */
|
||||
.message .author {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message {
|
||||
width: 400px;
|
||||
background-color: lightblue;
|
||||
margin: 10px 5px;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.remove {
|
||||
float: right;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// Generating demo data
|
||||
//------------------------------------------------------------------------------
|
||||
const AUTHORS = ["Aaron", "David", "Vincent"];
|
||||
const CONTENT = [
|
||||
"Lorem ipsum dolor sit amet",
|
||||
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem",
|
||||
"Excepteur sint occaecat cupidatat non proident"
|
||||
];
|
||||
|
||||
function chooseRandomly(array) {
|
||||
const index = Math.floor(Math.random() * array.length);
|
||||
return array[index];
|
||||
}
|
||||
|
||||
let nextId = 1;
|
||||
export function buildData(n = 1000) {
|
||||
const data = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
let id = nextId++;
|
||||
data.push({
|
||||
id: id,
|
||||
author: chooseRandomly(AUTHORS),
|
||||
msg: `${id}: ${chooseRandomly(CONTENT)}`,
|
||||
likes: 0
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Measuring helpers
|
||||
//------------------------------------------------------------------------------
|
||||
export function formatNumber(n) {
|
||||
return Number(n).toFixed();
|
||||
}
|
||||
|
||||
let startTime;
|
||||
let lastMeasure;
|
||||
export function startMeasure(descr) {
|
||||
startTime = performance.now();
|
||||
lastMeasure = descr;
|
||||
}
|
||||
export function stopMeasure(cb) {
|
||||
let last = lastMeasure;
|
||||
if (lastMeasure) {
|
||||
window.setTimeout(function() {
|
||||
lastMeasure = null;
|
||||
const stop = performance.now();
|
||||
const delta = stop - startTime;
|
||||
const msg = `[${last}] took ${formatNumber(delta)}ms`;
|
||||
console.log(msg);
|
||||
if (cb) {
|
||||
cb({ msg, delta });
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import {
|
||||
buildData,
|
||||
startMeasure,
|
||||
stopMeasure,
|
||||
formatNumber
|
||||
} from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
Vue.component("likes-counter", {
|
||||
data: function() {
|
||||
return {
|
||||
counter: 0
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
increment() {
|
||||
this.counter++;
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<button @click="increment">Value: {{counter}}</button>
|
||||
</div>`
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
Vue.component("my-message", {
|
||||
props: ["msg"],
|
||||
methods: {
|
||||
removeMessage() {
|
||||
this.$emit("removeMessage", this.msg.id);
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="message">
|
||||
<span class="author">{{msg.author}}</span>
|
||||
<span class="msg">{{msg.msg}}</span>
|
||||
<button class="remove" @click="removeMessage">Remove</button>
|
||||
<likes-counter></likes-counter>
|
||||
</div>`
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
const App = {
|
||||
name: "App",
|
||||
data() {
|
||||
return { messages: [], multipleFlag: false, clearAfterFlag: false };
|
||||
},
|
||||
mounted() {
|
||||
this.log(`Benchmarking Vue v${Vue.version}`);
|
||||
},
|
||||
methods: {
|
||||
addMessages(n) {
|
||||
this.benchmark("add " + n, () => {
|
||||
const newMessages = buildData(n);
|
||||
this.messages.push.apply(this.messages, newMessages);
|
||||
});
|
||||
},
|
||||
|
||||
clear() {
|
||||
this._benchmark("clear", () => {
|
||||
this.messages = [];
|
||||
});
|
||||
},
|
||||
|
||||
clearLog() {
|
||||
this.$refs.log.innerHTML = "";
|
||||
},
|
||||
|
||||
updateSomeMessages() {
|
||||
this.benchmark("update every 10th", () => {
|
||||
const messages = this.messages;
|
||||
for (let i = 0; i < this.messages.length; i += 10) {
|
||||
messages[i].author += "!!!";
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
removeMessage(id) {
|
||||
startMeasure("remove message");
|
||||
const index = this.messages.findIndex(m => m.id === id);
|
||||
this.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
},
|
||||
log(str, isBold) {
|
||||
const div = document.createElement("div");
|
||||
if (isBold) {
|
||||
div.classList.add("bold");
|
||||
}
|
||||
div.textContent = `> ${str}`;
|
||||
this.$refs.log.appendChild(div);
|
||||
this.$refs.log.scrollTop = this.$refs.log.scrollHeight;
|
||||
},
|
||||
benchmark(message, fn, callback) {
|
||||
if (this.multipleFlag) {
|
||||
const N = 20;
|
||||
let n = N;
|
||||
let total = 0;
|
||||
let cb = info => {
|
||||
let finalize = () => {
|
||||
n--;
|
||||
total += info.delta;
|
||||
if (n === 0) {
|
||||
const avg = total / N;
|
||||
this.log(`Average: ${formatNumber(avg)}ms`, true);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
this._benchmark(message, fn, cb);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.clearAfterFlag) {
|
||||
this._benchmark(
|
||||
"clear",
|
||||
() => {
|
||||
this.messages = [];
|
||||
},
|
||||
finalize,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
this._benchmark(message, fn, cb);
|
||||
} else {
|
||||
this._benchmark(message, fn, callback);
|
||||
}
|
||||
},
|
||||
|
||||
_benchmark(message, fn, cb, log = true) {
|
||||
setTimeout(() => {
|
||||
startMeasure(message);
|
||||
fn();
|
||||
stopMeasure(info => {
|
||||
if (log) {
|
||||
this.log(info.msg);
|
||||
}
|
||||
if (cb) {
|
||||
cb(info);
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="main">
|
||||
<div class="left-thing">
|
||||
<div class="title">Actions</div>
|
||||
<div class="panel">
|
||||
<button @click="addMessages(100)">Add 100 messages</button>
|
||||
<button @click="addMessages(1000)">Add 1k messages</button>
|
||||
<button @click="addMessages(10000)">Add 10k messages</button>
|
||||
<button @click="addMessages(30000)">Add 30k messages</button>
|
||||
<button @click="updateSomeMessages">Update every 10th message</button>
|
||||
<button @click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="flags">
|
||||
<div>
|
||||
<input type="checkbox" id="multipleflag" v-model="multipleFlag"/>
|
||||
<label for="multipleflag">Do it 20x</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="clearFlag" v-model="clearAfterFlag"/>
|
||||
<label for="clearFlag">Clear after</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">Number of messages: {{messages.length}}</div>
|
||||
<hr/>
|
||||
<div class="title">Log <span class="clear-log" @click="clearLog">(clear)</span></div>
|
||||
<div class="log">
|
||||
<div class="log-content" ref="log"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<my-message v-for="msg in messages" v-bind:key="msg.id" v-bind:msg="msg" @removeMessage="removeMessage"></my-message>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
new Vue({
|
||||
render: h => h(App)
|
||||
}).$mount(`#main`);
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Vue.js Benchmark</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='vue.min.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user