Compare commits
157 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb7d25ba0e | |||
| 5187f01c44 | |||
| 521111644c | |||
| c2728c9daf | |||
| 871dad6a13 | |||
| b620502a0f | |||
| 89cb00cc83 | |||
| 56041bc133 | |||
| e788e361c7 | |||
| 9d378b0e7b | |||
| fd3c194525 | |||
| ac9ccb81ca | |||
| 2b5cea944b | |||
| cf8039f643 | |||
| aec2373e6d | |||
| a9be149e1e | |||
| b365ea5c9c | |||
| b31fa81083 | |||
| 9b656fd9e4 | |||
| 17f4823b13 | |||
| d3bc101177 | |||
| 4287beae19 | |||
| a073c68685 | |||
| 7078d64049 | |||
| ccd31f12d9 | |||
| aff4019cde | |||
| 5ecb4809ff | |||
| c2daecc07b | |||
| fcda17c8e9 | |||
| aeed79c7e5 | |||
| 1c5b6f2573 | |||
| cd9b72158b | |||
| 7fc552e2f8 | |||
| e6768501cd | |||
| 6b2486473f | |||
| 7e687234bf | |||
| 968e96ad08 | |||
| 26c7856d5d | |||
| b8d09e523d | |||
| 04c2808701 | |||
| 15c2604df1 | |||
| 3e11fe6b12 | |||
| 20c6cacb4e | |||
| eb2b32ab60 | |||
| 2a223288d4 | |||
| 1272278225 | |||
| f502dd732e | |||
| 9c2d957525 | |||
| f8bb86820e | |||
| 0cde4b8737 | |||
| 66a801393f | |||
| e7f405cc97 | |||
| 55c48b2b12 | |||
| 2eb151c92d | |||
| 7952f31e63 | |||
| 11e4e67599 | |||
| 7b7a6de373 | |||
| b63d1e28b2 | |||
| c0667a11c6 | |||
| fddb1ec924 | |||
| e6c3b62ef0 | |||
| 97b69f164f | |||
| 33dfeb1b41 | |||
| dd292472b9 | |||
| 9b18b57fdf | |||
| 68f491cd32 | |||
| 7b3e39ba27 | |||
| 61fc3f4fdc | |||
| 7b454dae66 | |||
| 70101e4c66 | |||
| 5ef405293a | |||
| 9dcbbe54eb | |||
| e94428a186 | |||
| 941190dfa8 | |||
| a53e42518f | |||
| b7c37ca69a | |||
| 34489a2494 | |||
| 398df543fe | |||
| aa6a4a5d46 | |||
| db3499f5e5 | |||
| 166cada8ff | |||
| acbe316689 | |||
| fb013ccc72 | |||
| fee3eecd7f | |||
| 6037018dd2 | |||
| eec7cc4ea7 | |||
| 035895b043 | |||
| ff5ef82ea2 | |||
| 0048205636 | |||
| 0d341d9e7b | |||
| 4c7f572dbe | |||
| 610c272104 | |||
| 32e4565131 | |||
| 9b9c15e4a9 | |||
| b1690f19cc | |||
| 5dcee2564c | |||
| 752160fd85 | |||
| 3937966b74 | |||
| e7ebb92104 | |||
| c78e070636 | |||
| 610ed02373 | |||
| 4b23f51974 | |||
| b25e988628 | |||
| e0758c0e9e | |||
| ba34811f71 | |||
| 28672096a2 | |||
| 9823a4e7dd | |||
| 105ec7ced8 | |||
| 8f9ad987b9 | |||
| 6050827689 | |||
| 9b1ec5dc0e | |||
| 9b1c270501 | |||
| 9d99f8936b | |||
| 7bc9d34a64 | |||
| b1a3b322ee | |||
| ca688d8640 | |||
| ea9f533536 | |||
| 875ebdcfb0 | |||
| 2e07799250 | |||
| 0d9d21a5c1 | |||
| 836e12b1c5 | |||
| 7538aeae0e | |||
| 3e9ba9ca8e | |||
| e4c296a7d2 | |||
| 44748270da | |||
| 8b1dc4c43d | |||
| c105c6da38 | |||
| 3001420a1d | |||
| 432ff444a1 | |||
| aa3c88a6c4 | |||
| 601a98e649 | |||
| 23c7d19ef0 | |||
| 59c49b5833 | |||
| 2cca0bd819 | |||
| fbf4c4add2 | |||
| 78d6ff735e | |||
| a7f51fa666 | |||
| 5175f95289 | |||
| 3c9f4a8ae9 | |||
| 310730782c | |||
| 77a413d750 | |||
| fe31f93c94 | |||
| 9cc0f88e02 | |||
| 412fda10fd | |||
| aa441274c7 | |||
| ba20267151 | |||
| aabb7559b8 | |||
| 606d14a399 | |||
| c049022798 | |||
| cae32c5bd4 | |||
| 63128b7d29 | |||
| f9d810a42f | |||
| 1784a98b2c | |||
| 52be5ae85f | |||
| 0765b19367 | |||
| b8b5190bc6 | |||
| 9475de4d18 |
@@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x, 14.x, 16.x]
|
node-version: [20.x, 22.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|||||||
@@ -15,17 +15,21 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
#ide's
|
#ide's
|
||||||
.vscode
|
**/.vscode/*
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
# Extras temp file
|
|
||||||
/tools/owl.js
|
|
||||||
|
|
||||||
release-notes.md
|
release-notes.md
|
||||||
|
|
||||||
.rpt2_cache
|
.rpt2_cache
|
||||||
|
|
||||||
# useful in some cases
|
# useful in some cases
|
||||||
/temp
|
/temp
|
||||||
|
|
||||||
|
# owl-vision
|
||||||
|
*/owl-vision/out/
|
||||||
|
*/owl-vision/.vs/
|
||||||
|
**/*.vsix
|
||||||
|
!*/owl-vision/.vscode/launch.json
|
||||||
|
!*/owl-vision/.vscode/tasks.json
|
||||||
|
|||||||
@@ -47,3 +47,4 @@ Utility/helpers:
|
|||||||
- [`status`](reference/component.md#status-helper): utility function to get the status of a component (new, mounted or destroyed)
|
- [`status`](reference/component.md#status-helper): utility function to get the status of a component (new, mounted or destroyed)
|
||||||
- [`validate`](reference/utils.md#validate): validates if an object satisfies a specified schema
|
- [`validate`](reference/utils.md#validate): validates if an object satisfies a specified schema
|
||||||
- [`whenReady`](reference/utils.md#whenready): utility function to execute code when DOM is ready
|
- [`whenReady`](reference/utils.md#whenready): utility function to execute code when DOM is ready
|
||||||
|
- [`batched`](reference/utils.md#batched): utility function to batch function calls
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
- [API](#api)
|
- [API](#api)
|
||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
- [`mount` helper](#mount-helper)
|
- [`mount` helper](#mount-helper)
|
||||||
|
- [Roots](#roots)
|
||||||
- [Loading templates](#loading-templates)
|
- [Loading templates](#loading-templates)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
@@ -61,8 +62,13 @@ The `config` object is an object with some of the following keys:
|
|||||||
templates (see [translations](translations.md))
|
templates (see [translations](translations.md))
|
||||||
- **`templates (string | xml document)`**: all the templates that will be used by
|
- **`templates (string | xml document)`**: all the templates that will be used by
|
||||||
the components created by the application.
|
the components created by the application.
|
||||||
|
- **`getTemplate ((s: string) => Element | Function | string | void)`**: a function that will be called by owl when it
|
||||||
|
needs a template. If undefined is returned, owl looks into the app templates.
|
||||||
- **`warnIfNoStaticProps (boolean, default=false)`**: if true, Owl will log a warning
|
- **`warnIfNoStaticProps (boolean, default=false)`**: if true, Owl will log a warning
|
||||||
whenever it encounters a component that does not provide a [static props description](props.md#props-validation).
|
whenever it encounters a component that does not provide a [static props description](props.md#props-validation).
|
||||||
|
- **`customDirectives (object)`**: if given, the corresponding function on the object will be called
|
||||||
|
on the template custom directives: `t-custom-*` (see [Custom Directives](templates.md#custom-directives)).
|
||||||
|
- **`globalValues (object)`**: Global object of elements available at compilations.
|
||||||
|
|
||||||
## `mount` helper
|
## `mount` helper
|
||||||
|
|
||||||
@@ -90,6 +96,33 @@ Most of the time, the `mount` helper is more convenient, but whenever one needs
|
|||||||
a reference to the actual Owl App, then using the `App` class directly is
|
a reference to the actual Owl App, then using the `App` class directly is
|
||||||
possible.
|
possible.
|
||||||
|
|
||||||
|
## Roots
|
||||||
|
|
||||||
|
An application can have multiple roots. It is sometimes useful to instantiate
|
||||||
|
sub components in places that are not managed by Owl, such as an html editor
|
||||||
|
with dynamic content (the Knowledge application in Odoo).
|
||||||
|
|
||||||
|
To create a root, one can use the `createRoot` method, which takes two arguments:
|
||||||
|
|
||||||
|
- **`Component`**: a component class (Root component of the app)
|
||||||
|
- **`config (optional)`**: a config object that may contain a `props` object or a
|
||||||
|
`env` object.
|
||||||
|
|
||||||
|
The `createRoot` method returns an object with a `mount` method (same API as
|
||||||
|
the `App.mount` method), and a `destroy` method.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const root = app.createRoot(MyComponent, { props: { someProps: true } });
|
||||||
|
await root.mount(targetElement);
|
||||||
|
|
||||||
|
// later
|
||||||
|
root.destroy();
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that, like with owl `App`, it is the responsibility of the code that created
|
||||||
|
the root to properly destroy it (before it has been removed from the DOM!). Owl
|
||||||
|
has no way of doing it itself.
|
||||||
|
|
||||||
## Loading templates
|
## Loading templates
|
||||||
|
|
||||||
Most applications will need to load templates whenever they start. Here is
|
Most applications will need to load templates whenever they start. Here is
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ cleaning operation, since the component may be destroyed before it has even been
|
|||||||
mounted. The `willDestroy` hook is useful in that situation, since it is always
|
mounted. The `willDestroy` hook is useful in that situation, since it is always
|
||||||
called.
|
called.
|
||||||
|
|
||||||
The `onWillUnmount` hook is used to register a function that will be executed at
|
The `onWillDestroy` hook is used to register a function that will be executed at
|
||||||
this moment:
|
this moment:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@@ -451,5 +451,6 @@ console.log(status(component));
|
|||||||
// logs either:
|
// logs either:
|
||||||
// - 'new', if the component is new and has not been mounted yet
|
// - 'new', if the component is new and has not been mounted yet
|
||||||
// - 'mounted', if the component is currently mounted
|
// - 'mounted', if the component is currently mounted
|
||||||
|
// - 'cancelled', if the component has not been mounted yet but will be destroyed soon
|
||||||
// - 'destroyed' if the component is currently destroyed
|
// - 'destroyed' if the component is currently destroyed
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ that render its content, and a fallback if an error happened.
|
|||||||
```js
|
```js
|
||||||
class ErrorBoundary extends Component {
|
class ErrorBoundary extends Component {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
<t t-if="error" t-slot="fallback">An error occurred</t>
|
<t t-if="state.error" t-slot="fallback">An error occurred</t>
|
||||||
<t t-else="" t-slot="content"`;
|
<t t-else="" t-slot="default"/>`;
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
this.state = useState({ error: false });
|
this.state = useState({ error: false });
|
||||||
|
|||||||
@@ -190,12 +190,13 @@ will then be updated accordingly.
|
|||||||
### `useExternalListener`
|
### `useExternalListener`
|
||||||
|
|
||||||
The `useExternalListener` hook helps solve a very common problem: adding and removing
|
The `useExternalListener` hook helps solve a very common problem: adding and removing
|
||||||
a listener on some target whenever a component is mounted/unmounted. For example,
|
a listener on some target whenever a component is mounted/unmounted. It takes a target
|
||||||
|
as its first argument, forwards the other arguments to `addEventListener`. For example,
|
||||||
a dropdown menu (or its parent) may need to listen to a `click` event on `window`
|
a dropdown menu (or its parent) may need to listen to a `click` event on `window`
|
||||||
to be closed:
|
to be closed:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
useExternalListener(window, "click", this.closeMenu);
|
useExternalListener(window, "click", this.closeMenu, { capture: true });
|
||||||
```
|
```
|
||||||
|
|
||||||
### `useComponent`
|
### `useComponent`
|
||||||
|
|||||||
@@ -140,6 +140,28 @@ class SomeComponent extends Component {
|
|||||||
The `.bind` suffix also implies `.alike`, so these props will not cause additional
|
The `.bind` suffix also implies `.alike`, so these props will not cause additional
|
||||||
renderings.
|
renderings.
|
||||||
|
|
||||||
|
## Translatable props
|
||||||
|
|
||||||
|
When you need to pass a user-facing string to a subcomponent, you likely want it
|
||||||
|
to be translated. Unfortunately, because props are arbitrary expressions, it wouldn't
|
||||||
|
be practical for Owl to find out which parts of the expression are strings and translate
|
||||||
|
them, and it also makes it difficult for tooling to extract these strings to generate
|
||||||
|
terms to translate. While you can work around this issue by doing the translation in
|
||||||
|
JavaScript, or by using `t-set` with a body (the body of `t-set` is translated),
|
||||||
|
and passing the variable as a prop, this is a sufficiently common use case that Owl
|
||||||
|
provides a suffix for this purpose: `.translate`.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<t t-name="ParentComponent">
|
||||||
|
<Child someProp.translate="some message"/>
|
||||||
|
</t>
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the content of this attribute is _NOT_ treated as a JavaScript expression:
|
||||||
|
it is treated as a string, as if it was an attribute on an HTML element, and translated
|
||||||
|
before being passed to the component. If you need to interpolate some data into the
|
||||||
|
string, you will still have to do this in JavaScript.
|
||||||
|
|
||||||
## Dynamic Props
|
## Dynamic Props
|
||||||
|
|
||||||
The `t-props` directive can be used to specify totally dynamic props:
|
The `t-props` directive can be used to specify totally dynamic props:
|
||||||
@@ -238,7 +260,7 @@ class ComponentB extends owl.Component {
|
|||||||
count: {type: Number},
|
count: {type: Number},
|
||||||
messages: {
|
messages: {
|
||||||
type: Array,
|
type: Array,
|
||||||
element: {type: Object, shape: {id: Boolean, text: String }
|
element: {type: Object, shape: {id: Boolean, text: String }}
|
||||||
},
|
},
|
||||||
date: Date,
|
date: Date,
|
||||||
combinedVal: [Number, Boolean],
|
combinedVal: [Number, Boolean],
|
||||||
@@ -276,7 +298,8 @@ class ComponentB extends owl.Component {
|
|||||||
id: Number,
|
id: Number,
|
||||||
name: {type: String, optional: true},
|
name: {type: String, optional: true},
|
||||||
url: String
|
url: String
|
||||||
]}, // object, with keys id (number), name (string, optional) and url (string)
|
}
|
||||||
|
}, // object, with keys id (number), name (string, optional) and url (string)
|
||||||
someObj3: {
|
someObj3: {
|
||||||
type: Object,
|
type: Object,
|
||||||
values: { type: Array, element: String },
|
values: { type: Array, element: String },
|
||||||
@@ -297,6 +320,28 @@ class ComponentB extends owl.Component {
|
|||||||
|
|
||||||
Note: the props validation code is done by using the [validate utility function](utils.md#validate).
|
Note: the props validation code is done by using the [validate utility function](utils.md#validate).
|
||||||
|
|
||||||
|
### `slots` prop
|
||||||
|
|
||||||
|
If a component that uses [slots](slots.md) also lists or validates its props, then
|
||||||
|
you will have to explicitely allow the `slots` prop (with an `Object` type), or
|
||||||
|
allow extra props using the `*` notation mentioned above. This is because slots
|
||||||
|
are provided to a component [as props](slots.md#slots-and-props).
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class MyComponent extends Component {
|
||||||
|
static props = [someProp, slots?];
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyComponentWithValidation extends Component {
|
||||||
|
static props = {
|
||||||
|
someProp: {type: Number, optional: true},
|
||||||
|
slots : {type: Object, optional: true},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Good Practices
|
## Good Practices
|
||||||
|
|
||||||
A `props` object is a collection of values that come from the parent. As such,
|
A `props` object is a collection of values that come from the parent. As such,
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ This may seem counter-intuitive, but it makes perfect sense in the context of co
|
|||||||
```js
|
```js
|
||||||
class DoubleCounter extends Component {
|
class DoubleCounter extends Component {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
<t t-esc="state.selected + ': ' + state[state.selected].value"/>
|
<t t-esc="'selected: ' + state.selected + ', value: ' + state[state.selected]"/>
|
||||||
<button t-on-click="() => this.state.count1++">increment count 1</button>
|
<button t-on-click="() => this.state.count1++">increment count 1</button>
|
||||||
<button t-on-click="() => this.state.count2++">increment count 2</button>
|
<button t-on-click="() => this.state.count2++">increment count 2</button>
|
||||||
<button t-on-click="changeCounter">Switch counter</button>
|
<button t-on-click="changeCounter">Switch counter</button>
|
||||||
@@ -193,7 +193,7 @@ to be able to opt out of creating them in the first place. This is the purpose o
|
|||||||
### `markRaw`
|
### `markRaw`
|
||||||
|
|
||||||
Marks an object so that it is ignored by the reactivity system, meaning that if this object is ever
|
Marks an object so that it is ignored by the reactivity system, meaning that if this object is ever
|
||||||
part of a of a reactive object, it will be returned as is, and no keys in that object will be
|
part of a reactive object, it will be returned as is, and no keys in that object will be
|
||||||
observed.
|
observed.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ Slots can define a default content, in case the parent did not define them:
|
|||||||
## Dynamic Slots
|
## Dynamic Slots
|
||||||
|
|
||||||
The `t-slot` directive is actually able to use any expressions, using string
|
The `t-slot` directive is actually able to use any expressions, using string
|
||||||
interplolation:
|
interpolation:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<t t-slot="{{current}}" />
|
<t t-slot="{{current}}" />
|
||||||
@@ -175,7 +175,7 @@ class Notebook extends Component {
|
|||||||
<div class="notebook">
|
<div class="notebook">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<t t-foreach="tabNames" t-as="tab" t-key="tab_index">
|
<t t-foreach="tabNames" t-as="tab" t-key="tab_index">
|
||||||
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab">
|
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab_index">
|
||||||
<t t-esc="props.slots[tab].title"/>
|
<t t-esc="props.slots[tab].title"/>
|
||||||
</span>
|
</span>
|
||||||
</t>
|
</t>
|
||||||
@@ -201,16 +201,17 @@ use this `Notebook` component:
|
|||||||
|
|
||||||
```xml
|
```xml
|
||||||
<Notebook>
|
<Notebook>
|
||||||
<t t-set-slot="page1" title="'Page 1'">
|
<t t-set-slot="page1" title.translate="Page 1">
|
||||||
<div>this is in the page 1</div>
|
<div>this is in the page 1</div>
|
||||||
</t>
|
</t>
|
||||||
<t t-set-slot="page2" title="'Page 2'" hidden="somevalue">
|
<t t-set-slot="page2" title.translate="Page 2" hidden="somevalue">
|
||||||
<div>this is in the page 2</div>
|
<div>this is in the page 2</div>
|
||||||
</t>
|
</t>
|
||||||
</Notebook>
|
</Notebook>
|
||||||
```
|
```
|
||||||
|
|
||||||
Slot params works like normal props, so one can use the `.bind` suffix to
|
Slot params works like normal props, so one can use suffixes like `.translate`
|
||||||
|
when a prop is a user facing string and should be translated, or `.bind` to
|
||||||
bind a function if needed.
|
bind a function if needed.
|
||||||
|
|
||||||
## Slot scopes
|
## Slot scopes
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
- [Sub Templates](#sub-templates)
|
- [Sub Templates](#sub-templates)
|
||||||
- [Dynamic Sub Templates](#dynamic-sub-templates)
|
- [Dynamic Sub Templates](#dynamic-sub-templates)
|
||||||
- [Debugging](#debugging)
|
- [Debugging](#debugging)
|
||||||
|
- [Custom Directives](#custom-directives)
|
||||||
- [Fragments](#fragments)
|
- [Fragments](#fragments)
|
||||||
- [Inline templates](#inline-templates)
|
- [Inline templates](#inline-templates)
|
||||||
- [Rendering svg](#rendering-svg)
|
- [Rendering svg](#rendering-svg)
|
||||||
@@ -55,17 +56,19 @@ extensions.
|
|||||||
|
|
||||||
For reference, here is a list of all standard QWeb directives:
|
For reference, here is a list of all standard QWeb directives:
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| ------------------------------ | --------------------------------------------------------------- |
|
| ------------------------------ | ----------------------------------------------------------------------- |
|
||||||
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
||||||
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
|
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
|
||||||
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
||||||
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
|
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
|
||||||
| `t-foreach`, `t-as` | [Loops](#loops) |
|
| `t-foreach`, `t-as` | [Loops](#loops) |
|
||||||
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
|
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
|
||||||
| `t-call` | [Rendering sub templates](#sub-templates) |
|
| `t-call` | [Rendering sub templates](#sub-templates) |
|
||||||
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
||||||
| `t-translation` | [Disabling the translation of a node](translations.md) |
|
| `t-translation` | [Disabling the translation of a node](translations.md) |
|
||||||
|
| `t-translation-context` | [Context of translations within a node](translations.md) |
|
||||||
|
| `t-translation-context-*` | [Context of translation for a specific node attribute](translations.md) |
|
||||||
|
|
||||||
The component system in Owl requires additional directives, to express various
|
The component system in Owl requires additional directives, to express various
|
||||||
needs. Here is a list of all Owl specific directives:
|
needs. Here is a list of all Owl specific directives:
|
||||||
@@ -80,6 +83,7 @@ needs. Here is a list of all Owl specific directives:
|
|||||||
| `t-slot`, `t-set-slot`, `t-slot-scope` | [Rendering a slot](slots.md) |
|
| `t-slot`, `t-set-slot`, `t-slot-scope` | [Rendering a slot](slots.md) |
|
||||||
| `t-model` | [Form input bindings](input_bindings.md) |
|
| `t-model` | [Form input bindings](input_bindings.md) |
|
||||||
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
|
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
|
||||||
|
| `t-custom-*` | [Rendering nodes with custom directives](#custom-directives) |
|
||||||
|
|
||||||
## QWeb Template Reference
|
## QWeb Template Reference
|
||||||
|
|
||||||
@@ -189,6 +193,15 @@ The first `t-out` will act as a `t-esc` directive, which means that the content
|
|||||||
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
|
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
|
||||||
this will be injected as html.
|
this will be injected as html.
|
||||||
|
|
||||||
|
`markup` can also be used as a tag function, allowing the interpolated values to
|
||||||
|
be safely escaped:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const maliciousInput = "<script>alert('💥💥')</script>";
|
||||||
|
// <b><script>alert('💥💥')</script></b>
|
||||||
|
const value = markup`<b>${maliciousInput}</b>`;
|
||||||
|
```
|
||||||
|
|
||||||
### Setting Variables
|
### 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, ...
|
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, ...
|
||||||
@@ -376,15 +389,16 @@ An important difference should be made with the usual `QWeb` behaviour: Owl
|
|||||||
requires the presence of a `t-key` directive, to be able to properly reconcile
|
requires the presence of a `t-key` directive, to be able to properly reconcile
|
||||||
renderings.
|
renderings.
|
||||||
|
|
||||||
`t-foreach` can iterate on an array (the current item will be the current value)
|
`t-foreach` can iterate on any iterable, and also has special support for objects
|
||||||
or an object (the current item will be the current key).
|
and maps, it will expose the key of the current iteration as the contents of the
|
||||||
|
`t-as`, and the corresponding value with the same name and the suffix `_value`.
|
||||||
|
|
||||||
In addition to the name passed via t-as, `t-foreach` provides a few other
|
In addition to the name passed via t-as, `t-foreach` provides a few other useful
|
||||||
variables for various data points (note: `$as` will be replaced with the name
|
variables (note: `$as` will be replaced with the name passed to `t-as`):
|
||||||
passed to `t-as`):
|
|
||||||
|
|
||||||
- `$as_value`: the current iteration value, identical to `$as` for lists and
|
- `$as_value`: the current iteration value, identical to `$as` for arrays and
|
||||||
integers, but for objects, it provides the value (where `$as` provides the key)
|
other iterables, but for objects and maps, 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_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
|
- `$as_first`: whether the current item is the first of the iteration
|
||||||
(equivalent to `$as_index == 0`)
|
(equivalent to `$as_index == 0`)
|
||||||
@@ -587,6 +601,35 @@ will stop execution if the browser dev tools are open.
|
|||||||
|
|
||||||
will print 42 to the console.
|
will print 42 to the console.
|
||||||
|
|
||||||
|
### Custom Directives
|
||||||
|
|
||||||
|
Owl 2 supports the declaration of custom directives. To use them, an Object of functions needs to be configured on the owl APP:
|
||||||
|
|
||||||
|
```js
|
||||||
|
new App(..., {
|
||||||
|
customDirectives: {
|
||||||
|
test_directive: function (el, value) {
|
||||||
|
el.setAttribute("t-on-click", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The functions will be called when a custom directive with the name of the
|
||||||
|
function is found. The original element will be replaced with the one
|
||||||
|
modified by the function.
|
||||||
|
This :
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-custom-test_directive="click" />
|
||||||
|
```
|
||||||
|
|
||||||
|
will be replaced by :
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-on-click="value"/>
|
||||||
|
```
|
||||||
|
|
||||||
## Fragments
|
## Fragments
|
||||||
|
|
||||||
Owl 2 supports templates with an arbitrary number of root elements, or even just
|
Owl 2 supports templates with an arbitrary number of root elements, or even just
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
# 🦉 Translations 🦉
|
# 🦉 Translations 🦉
|
||||||
|
|
||||||
If properly setup, Owl can translate all rendered templates. To do
|
If properly setup, Owl can translate all rendered templates. To do
|
||||||
so, it needs a translate function, which takes a string and returns a string.
|
so, it needs a translate function, which takes
|
||||||
|
|
||||||
|
- a string (the term to translate)
|
||||||
|
- a string (the translation context of the term)
|
||||||
|
and returns a string.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const translations = {
|
const translations = {
|
||||||
hello: "bonjour",
|
fr: {
|
||||||
yes: "oui",
|
hello: "bonjour",
|
||||||
no: "non",
|
yes: "oui",
|
||||||
|
no: "non",
|
||||||
|
},
|
||||||
|
pt: {
|
||||||
|
hello: "bom dia",
|
||||||
|
yes: "sim",
|
||||||
|
no: "não",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const translateFn = (str) => translations[str] || str;
|
const translateFn = (str, ctx) => translations[ctx]?.[str] || str;
|
||||||
|
|
||||||
const app = new App(Root, { templates, tranaslateFn });
|
const app = new App(Root, { templates, tranaslateFn });
|
||||||
// ...
|
// ...
|
||||||
@@ -27,6 +38,11 @@ Once setup, all rendered templates will be translated using `translateFn`:
|
|||||||
`placeholder`, `label` and `alt`,
|
`placeholder`, `label` and `alt`,
|
||||||
- translating text nodes can be disabled with the special attribute `t-translation`,
|
- translating text nodes can be disabled with the special attribute `t-translation`,
|
||||||
if its value is `off`.
|
if its value is `off`.
|
||||||
|
- the translate function receives as second parameter a context that can be used
|
||||||
|
to contextualized the translation. That context can be set globally on a node
|
||||||
|
and its children by using `t-translation-context`. If a specific node
|
||||||
|
attribute `x` needs another context, that context can be specified with a
|
||||||
|
special directive `t-translation-context-x`.
|
||||||
|
|
||||||
So, with the above `translateFn`, the following templates:
|
So, with the above `translateFn`, the following templates:
|
||||||
|
|
||||||
@@ -46,6 +62,22 @@ will be rendered as:
|
|||||||
<input placeholder="bonjour" other="yes"/>
|
<input placeholder="bonjour" other="yes"/>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
and the following template:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-translation-context="fr" title="hello">hello</div>
|
||||||
|
<div>Are you sure?</div>
|
||||||
|
<input t-translation-context-placeholder="pt" placeholder="hello" other="yes"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
will be rendered as:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div title="bonjour">bonjour</div>
|
||||||
|
<div>Are you sure?</div>
|
||||||
|
<input placeholder="bom dia" other="yes"/>
|
||||||
|
```
|
||||||
|
|
||||||
Note that the translation is done during the compilation of the template, not
|
Note that the translation is done during the compilation of the template, not
|
||||||
when it is rendered.
|
when it is rendered.
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ functions are all available in the `owl.utils` namespace.
|
|||||||
- [`loadFile`](#loadfile): loading a file (useful for templates)
|
- [`loadFile`](#loadfile): loading a file (useful for templates)
|
||||||
- [`EventBus`](#eventbus): a simple EventBus
|
- [`EventBus`](#eventbus): a simple EventBus
|
||||||
- [`validate`](#validate): a validation function
|
- [`validate`](#validate): a validation function
|
||||||
|
- [`batched`](#batched): batch function calls
|
||||||
|
|
||||||
## `whenReady`
|
## `whenReady`
|
||||||
|
|
||||||
@@ -78,3 +79,22 @@ validate(
|
|||||||
// - 'id' is missing (should be a number),
|
// - 'id' is missing (should be a number),
|
||||||
// - 'url' is missing (should be a boolean or list of numbers),
|
// - 'url' is missing (should be a boolean or list of numbers),
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `batched`
|
||||||
|
|
||||||
|
The `batched` function creates a batched version of a callback so that multiple calls to it within the same microtick will only result in a single invocation of the original callback.
|
||||||
|
|
||||||
|
```js
|
||||||
|
function hello() {
|
||||||
|
console.log("hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchedHello = batched(hello);
|
||||||
|
batchedHello();
|
||||||
|
// Nothing is logged
|
||||||
|
batchedHello();
|
||||||
|
// Still not logged
|
||||||
|
|
||||||
|
await Promise.resolve(); // Await the next microtick
|
||||||
|
// "hello" is logged only once
|
||||||
|
```
|
||||||
|
|||||||
@@ -35,13 +35,13 @@ The components tab is separated into two sub windows: the components tree in the
|
|||||||
the component details in the right. The components tree will display all the different
|
the component details in the right. The components tree will display all the different
|
||||||
components that are present in the tab in the form of a tree. The root of this tree is
|
components that are present in the tab in the form of a tree. The root of this tree is
|
||||||
actually the app which is not a component but can still be inspected by the devtools like
|
actually the app which is not a component but can still be inspected by the devtools like
|
||||||
one. There can also be multiple apps loaded in the page like in the following:
|
one. There can also be multiple apps loaded in the page like in website:
|
||||||
|
|
||||||
<img src="screenshots/multi_apps.png"/>
|
<img src="screenshots/multi_apps.png"/>
|
||||||
|
|
||||||
There is a convenient search bar at the top of the components tree which will help finding
|
There is a convenient search bar at the top of the components tree which will help finding
|
||||||
the components tou want in the tree and also, an element picker can be used to directly select
|
the components tou want in the tree and also, an element picker can be used to directly select
|
||||||
the component you want to focus on in the page which is especially useful when trying to find
|
the component you want to focus on in the page which is especially useful when 1trying to find
|
||||||
what you want. Just click on the elements picker icon and click on the element you want to focus
|
what you want. Just click on the elements picker icon and click on the element you want to focus
|
||||||
on in the page and it will be selected in the devtools accordingly. Hovering any element in the
|
on in the page and it will be selected in the devtools accordingly. Hovering any element in the
|
||||||
page in this mode will highlight it and the same happens anytime in the components tree.
|
page in this mode will highlight it and the same happens anytime in the components tree.
|
||||||
@@ -64,31 +64,40 @@ as its env, props, observed states and all the other variables that are present
|
|||||||
While the props and the env are already present on the actual instance of the component and are
|
While the props and the env are already present on the actual instance of the component and are
|
||||||
pretty explicit by themselves, the observed state value is a bit more complicated to grasp.
|
pretty explicit by themselves, the observed state value is a bit more complicated to grasp.
|
||||||
|
|
||||||
The observed state is actually information about which variables are observed by the component
|
The observed state is actually information about which variables are being observed by the component:
|
||||||
which will trigger a rerender of the component when it is modified. The keys represent which part of the
|
when any property of a reactive object is being read by the component, the component will subscribe
|
||||||
variable is actually observed and the target is the actual variable. For simplicity, the properties
|
to this property which means it will listen to any change that can occur on the property and render
|
||||||
that are not observed by the component are greyed out while the others are in bold. This means that
|
when such a change occurs. This can be visualized easily within the devtools inside of the observed
|
||||||
editing bold ones will trigger a rerender while the greyed out ones will not.
|
state section: observed properties of the reactive object(s) are displayed in bold while the others
|
||||||
|
are greyed out. Do keep in mind that a greyed out property in the observed state of one component
|
||||||
|
may be observed by another and the other way around is also possible. Here is an example for some
|
||||||
|
user Field component:
|
||||||
|
|
||||||
<img src="screenshots/states.png"/>
|
<img src="screenshots/states.png"/>
|
||||||
|
|
||||||
In the given example, we have two keys/target pairs for two different variables. The first one indicates
|
Navigation inside the properties is also similar to the one in console variables: properties have
|
||||||
that adding or removing an element to the array will trigger a rerender since the length will have changed.
|
|
||||||
Replacing the element at index 0, 1 or 2 will also have the same effect as implied by the keys. It doesn't
|
|
||||||
mean that editing the properties of element at index 0, 1 or 2 will rerender the component though. It may
|
|
||||||
be the case for some but this will be described in another keys/target pair. The second keys/target pair
|
|
||||||
is actually the element at index 0 of the first pair. It only has id in the keys meaning that only the
|
|
||||||
id property will actually trigger a rerender the component when modified. Be aware however that the other
|
|
||||||
properties may be in the observed state of another component like a child one in this case. A greyed out
|
|
||||||
property only implies it is not reactive for the selected component and not for the others.
|
|
||||||
|
|
||||||
The navigation inside the properties is also similar to the one in console variables: properties have
|
|
||||||
their prototype displayed and getters will get their value when clicked on (...). It is also possible to
|
their prototype displayed and getters will get their value when clicked on (...). It is also possible to
|
||||||
send any property to the console using the right-click context menu on it and functions can be inspected
|
send any property to the console using the right-click context menu on it and functions can be inspected
|
||||||
in the sources tab as well.
|
in the sources tab as well.
|
||||||
|
|
||||||
<img src="screenshots/function_menu.png"/>
|
<img src="screenshots/function_menu.png"/>
|
||||||
|
|
||||||
|
Using the right-click context menu on a property also allows to observe variables. Observed variables will
|
||||||
|
be sent to a dedicated section of the details window and their value will be refreshed every 200ms. These
|
||||||
|
variables are only shown when they are found and their access path will be kept in memory inside the
|
||||||
|
browser so that it will always persist until the user decides to stop observing the variable. As in the
|
||||||
|
browser's devtools, observed objects are displayed in reduced form and cannot be interacted with. It is
|
||||||
|
still possible to send them to the console or remove them from the list using right-click.
|
||||||
|
|
||||||
|
<img src="screenshots/observe_variables.png"/>
|
||||||
|
|
||||||
|
The last section of the details window is filled with the component's lifecycle hooks. Using right click on
|
||||||
|
them allows to place breakpoints inside the hook (either on its instance or class, hooks like mounted and
|
||||||
|
willStart cannot have instance-based breakpoints because they will never trigger). Conditions in conditional
|
||||||
|
breakpoints will be evaluated in the context of the component's definition.
|
||||||
|
|
||||||
|
<img src="screenshots/hooks.png"/>
|
||||||
|
|
||||||
There are several icons available to perform several of the actions described before in the components
|
There are several icons available to perform several of the actions described before in the components
|
||||||
tree context menu and all these actions are also available by opening the menu by right-clicking on the
|
tree context menu and all these actions are also available by opening the menu by right-clicking on the
|
||||||
component's name. Using the left click on the component's name will focus it in the components tree.
|
component's name. Using the left click on the component's name will focus it in the components tree.
|
||||||
@@ -96,8 +105,10 @@ component's name. Using the left click on the component's name will focus it in
|
|||||||
It is also possible to edit any of the leaf node properties. To do so, you must double click on the
|
It is also possible to edit any of the leaf node properties. To do so, you must double click on the
|
||||||
property's value and modify it using the freshly created input then press enter to apply the changes.
|
property's value and modify it using the freshly created input then press enter to apply the changes.
|
||||||
Do note that the modified values should be written in JSON format in order to be valid (examples:
|
Do note that the modified values should be written in JSON format in order to be valid (examples:
|
||||||
89, "yes", undefined, null, \["hello", 15\], {"a": 1}, true, ...). Whether it has an impact on the
|
89, "yes", undefined, null, \["hello", 15\], {"a": 1}, true, ...). Editing any value will produce a
|
||||||
component or not and whether it produces an error is the responsability of the user.
|
manual render of the component (or the root component of the application in the case of env values).
|
||||||
|
Whether the edition has an impact on the component or not and whether it produces an error is the
|
||||||
|
responsability of the user.
|
||||||
|
|
||||||
<img src="screenshots/edit.png"/>
|
<img src="screenshots/edit.png"/>
|
||||||
|
|
||||||
@@ -117,7 +128,8 @@ are intercepted by the devtools using the record button.
|
|||||||
The second button is used to clear all the events that have been recorded. The select can be used to
|
The second button is used to clear all the events that have been recorded. The select can be used to
|
||||||
switch between the tree view (which shows the causality between renders) and the events log view which
|
switch between the tree view (which shows the causality between renders) and the events log view which
|
||||||
simply displays the events in the exact order they were triggered. In this view, you can expand the create,
|
simply displays the events in the exact order they were triggered. In this view, you can expand the create,
|
||||||
update and destroy events which reveals the component that initiated the event.
|
update and destroy events which reveals the component that initiated the event. Also, a transition line will
|
||||||
|
appear each time a new animation frame has been loaded between events.
|
||||||
|
|
||||||
<img src="screenshots/events_log.png"/>
|
<img src="screenshots/events_log.png"/>
|
||||||
|
|
||||||
@@ -131,20 +143,29 @@ There is also the Trace Renderings and Trace Subscriptions features. These featu
|
|||||||
recording of events and have no effect on the profiler tab. The Trace Renderings option is used to log in
|
recording of events and have no effect on the profiler tab. The Trace Renderings option is used to log in
|
||||||
the console all the render events and allows to show their traceback information. Similarly, the Trace
|
the console all the render events and allows to show their traceback information. Similarly, the Trace
|
||||||
Subscriptions option logs all the properties that caused a render event and also allows to see the traceback
|
Subscriptions option logs all the properties that caused a render event and also allows to see the traceback
|
||||||
of the modification
|
of the modification.
|
||||||
|
|
||||||
<img src="screenshots/trace_rendering.png"/>
|
<img src="screenshots/trace_rendering.png"/>
|
||||||
<img src="screenshots/trace_subscriptions.png"/>
|
<img src="screenshots/trace_subscriptions.png"/>
|
||||||
|
|
||||||
|
The Owl Devtools also allow to inspect iframes coded in Owl: when an Owl iframe is detected in the page,
|
||||||
|
the iframe selector will appear next to the tabs. This allows to switch from an iframe to another easily.
|
||||||
|
Be aware that switching iframes will clear all record events from the profiler tab. Iframes detection is
|
||||||
|
currently not working in the firefox version, we are aware of this issue and will try to address it in the
|
||||||
|
future.
|
||||||
|
|
||||||
|
<img src="screenshots/iframes.png"/>
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
The owl devtools extension has a dark mode feature which defaults to your general devtools settings and can
|
The owl devtools extension has a dark mode feature which defaults to your general devtools settings and can
|
||||||
be toggled using the sun/moon icon at the top-right corner of the tab. All the examples above were created
|
be toggled using the sun/moon icon at the top-right corner of the tab. There is also a refresh button to
|
||||||
with the dark mode enabled. There is also a refresh button to completely reset the owl devtools.
|
completely reset the owl devtools.
|
||||||
|
|
||||||
<img src="screenshots/darkmode.png"/>
|
<img src="screenshots/darkmode.png"/>
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
If the feedback from the page to the devtools seems to be cut, just close the devtools and refresh the page.
|
If the feedback from the page to the devtools seems to be cut, you can first try to use the refresh
|
||||||
|
button mentioned above but if it still doesn't seem to work, just close the devtools and refresh the page.
|
||||||
This will eventually happen any time a tab stays opened for too long without being refreshed.
|
This will eventually happen any time a tab stays opened for too long without being refreshed.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 333 KiB After Width: | Height: | Size: 314 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 311 KiB After Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 488 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 182 KiB |
@@ -0,0 +1,72 @@
|
|||||||
|
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
|
||||||
|
|
||||||
|
/* Tomorrow Comment */
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-quote {
|
||||||
|
color: #8e908c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tomorrow Red */
|
||||||
|
.hljs-variable,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-tag,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-selector-class,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-deletion {
|
||||||
|
color: #c82829;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tomorrow Orange */
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-builtin-name,
|
||||||
|
.hljs-literal,
|
||||||
|
.hljs-type,
|
||||||
|
.hljs-params,
|
||||||
|
.hljs-meta,
|
||||||
|
.hljs-link {
|
||||||
|
color: #f5871f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tomorrow Yellow */
|
||||||
|
.hljs-attribute {
|
||||||
|
color: #eab700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tomorrow Green */
|
||||||
|
.hljs-string,
|
||||||
|
.hljs-symbol,
|
||||||
|
.hljs-bullet,
|
||||||
|
.hljs-addition {
|
||||||
|
color: #718c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tomorrow Blue */
|
||||||
|
.hljs-title,
|
||||||
|
.hljs-section {
|
||||||
|
color: #4271ae;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tomorrow Purple */
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-selector-tag {
|
||||||
|
color: #8959a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
background: white;
|
||||||
|
color: #4d4d4c;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
html, body {
|
||||||
|
background-color: #fefefe;
|
||||||
|
font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 3rem;
|
||||||
|
grid-template-areas: "header" "nav" "main" "footer";
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
height: 100%;
|
||||||
|
grid-template-rows: 1fr 3rem 1fr 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 800px) {
|
||||||
|
body {
|
||||||
|
grid-template-rows: 1fr 3rem 1fr 5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #00A09D;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover, a:active, a:visited {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
grid-area: header;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 3rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
hgroup > p {
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
grid-area: nav;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
grid-area: main;
|
||||||
|
/*justify-self: center;*/
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
/*max-width: 80rem !important;*/
|
||||||
|
/*margin: 3rem 0;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
grid-area: footer;
|
||||||
|
padding-bottom: 3rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
header, nav, footer {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
article {
|
||||||
|
padding: 3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
article h3 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.brand {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.design {
|
||||||
|
background-color: #875A7B;
|
||||||
|
color: #f0eeee;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .homepage-logo {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 40rem) {
|
||||||
|
.row {
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: initial;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column {
|
||||||
|
margin-bottom: initial;
|
||||||
|
padding: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 60rem) {
|
||||||
|
.row {
|
||||||
|
flex-direction: row;
|
||||||
|
margin-left: -1.0rem;
|
||||||
|
width: calc(100% + 2.0rem);
|
||||||
|
}
|
||||||
|
.row .column {
|
||||||
|
margin-bottom: inherit;
|
||||||
|
padding: 0 1.0rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,602 @@
|
|||||||
|
/*!
|
||||||
|
* Milligram v1.3.0
|
||||||
|
* https://milligram.github.io
|
||||||
|
*
|
||||||
|
* Copyright (c) 2017 CJ Patoilo
|
||||||
|
* Licensed under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
*,
|
||||||
|
*:after,
|
||||||
|
*:before {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 62.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: #606c76;
|
||||||
|
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
|
||||||
|
font-size: 1.6em;
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: .01em;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 0.3rem solid #d1d1d1;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button,
|
||||||
|
button,
|
||||||
|
input[type='button'],
|
||||||
|
input[type='reset'],
|
||||||
|
input[type='submit'] {
|
||||||
|
background-color: #9b4dca;
|
||||||
|
border: 0.1rem solid #9b4dca;
|
||||||
|
border-radius: .4rem;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
height: 3.8rem;
|
||||||
|
letter-spacing: .1rem;
|
||||||
|
line-height: 3.8rem;
|
||||||
|
padding: 0 3.0rem;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:focus, .button:hover,
|
||||||
|
button:focus,
|
||||||
|
button:hover,
|
||||||
|
input[type='button']:focus,
|
||||||
|
input[type='button']:hover,
|
||||||
|
input[type='reset']:focus,
|
||||||
|
input[type='reset']:hover,
|
||||||
|
input[type='submit']:focus,
|
||||||
|
input[type='submit']:hover {
|
||||||
|
background-color: #606c76;
|
||||||
|
border-color: #606c76;
|
||||||
|
color: #fff;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button[disabled],
|
||||||
|
button[disabled],
|
||||||
|
input[type='button'][disabled],
|
||||||
|
input[type='reset'][disabled],
|
||||||
|
input[type='submit'][disabled] {
|
||||||
|
cursor: default;
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button[disabled]:focus, .button[disabled]:hover,
|
||||||
|
button[disabled]:focus,
|
||||||
|
button[disabled]:hover,
|
||||||
|
input[type='button'][disabled]:focus,
|
||||||
|
input[type='button'][disabled]:hover,
|
||||||
|
input[type='reset'][disabled]:focus,
|
||||||
|
input[type='reset'][disabled]:hover,
|
||||||
|
input[type='submit'][disabled]:focus,
|
||||||
|
input[type='submit'][disabled]:hover {
|
||||||
|
background-color: #9b4dca;
|
||||||
|
border-color: #9b4dca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-outline,
|
||||||
|
button.button-outline,
|
||||||
|
input[type='button'].button-outline,
|
||||||
|
input[type='reset'].button-outline,
|
||||||
|
input[type='submit'].button-outline {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #9b4dca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-outline:focus, .button.button-outline:hover,
|
||||||
|
button.button-outline:focus,
|
||||||
|
button.button-outline:hover,
|
||||||
|
input[type='button'].button-outline:focus,
|
||||||
|
input[type='button'].button-outline:hover,
|
||||||
|
input[type='reset'].button-outline:focus,
|
||||||
|
input[type='reset'].button-outline:hover,
|
||||||
|
input[type='submit'].button-outline:focus,
|
||||||
|
input[type='submit'].button-outline:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: #606c76;
|
||||||
|
color: #606c76;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover,
|
||||||
|
button.button-outline[disabled]:focus,
|
||||||
|
button.button-outline[disabled]:hover,
|
||||||
|
input[type='button'].button-outline[disabled]:focus,
|
||||||
|
input[type='button'].button-outline[disabled]:hover,
|
||||||
|
input[type='reset'].button-outline[disabled]:focus,
|
||||||
|
input[type='reset'].button-outline[disabled]:hover,
|
||||||
|
input[type='submit'].button-outline[disabled]:focus,
|
||||||
|
input[type='submit'].button-outline[disabled]:hover {
|
||||||
|
border-color: inherit;
|
||||||
|
color: #9b4dca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-clear,
|
||||||
|
button.button-clear,
|
||||||
|
input[type='button'].button-clear,
|
||||||
|
input[type='reset'].button-clear,
|
||||||
|
input[type='submit'].button-clear {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: #9b4dca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-clear:focus, .button.button-clear:hover,
|
||||||
|
button.button-clear:focus,
|
||||||
|
button.button-clear:hover,
|
||||||
|
input[type='button'].button-clear:focus,
|
||||||
|
input[type='button'].button-clear:hover,
|
||||||
|
input[type='reset'].button-clear:focus,
|
||||||
|
input[type='reset'].button-clear:hover,
|
||||||
|
input[type='submit'].button-clear:focus,
|
||||||
|
input[type='submit'].button-clear:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: #606c76;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover,
|
||||||
|
button.button-clear[disabled]:focus,
|
||||||
|
button.button-clear[disabled]:hover,
|
||||||
|
input[type='button'].button-clear[disabled]:focus,
|
||||||
|
input[type='button'].button-clear[disabled]:hover,
|
||||||
|
input[type='reset'].button-clear[disabled]:focus,
|
||||||
|
input[type='reset'].button-clear[disabled]:hover,
|
||||||
|
input[type='submit'].button-clear[disabled]:focus,
|
||||||
|
input[type='submit'].button-clear[disabled]:hover {
|
||||||
|
color: #9b4dca;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: #f4f5f6;
|
||||||
|
border-radius: .4rem;
|
||||||
|
font-size: 86%;
|
||||||
|
margin: 0 .2rem;
|
||||||
|
padding: .2rem .5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: #f4f5f6;
|
||||||
|
border-left: 0.3rem solid #9b4dca;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > code {
|
||||||
|
border-radius: 0;
|
||||||
|
display: block;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-top: 0.1rem solid #f4f5f6;
|
||||||
|
margin: 3.0rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='email'],
|
||||||
|
input[type='number'],
|
||||||
|
input[type='password'],
|
||||||
|
input[type='search'],
|
||||||
|
input[type='tel'],
|
||||||
|
input[type='text'],
|
||||||
|
input[type='url'],
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0.1rem solid #d1d1d1;
|
||||||
|
border-radius: .4rem;
|
||||||
|
box-shadow: none;
|
||||||
|
box-sizing: inherit;
|
||||||
|
height: 3.8rem;
|
||||||
|
padding: .6rem 1.0rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='email']:focus,
|
||||||
|
input[type='number']:focus,
|
||||||
|
input[type='password']:focus,
|
||||||
|
input[type='search']:focus,
|
||||||
|
input[type='tel']:focus,
|
||||||
|
input[type='text']:focus,
|
||||||
|
input[type='url']:focus,
|
||||||
|
textarea:focus,
|
||||||
|
select:focus {
|
||||||
|
border-color: #9b4dca;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;
|
||||||
|
padding-right: 3.0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:focus {
|
||||||
|
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#9b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>');
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 6.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label,
|
||||||
|
legend {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'],
|
||||||
|
input[type='radio'] {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-inline {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-left: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 112.0rem;
|
||||||
|
padding: 0 2.0rem;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-no-padding {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-no-padding > .column {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-wrap {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-top {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-bottom {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-stretch {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-baseline {
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column {
|
||||||
|
display: block;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
margin-left: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-10 {
|
||||||
|
margin-left: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-20 {
|
||||||
|
margin-left: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-25 {
|
||||||
|
margin-left: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-33, .row .column.column-offset-34 {
|
||||||
|
margin-left: 33.3333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-50 {
|
||||||
|
margin-left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-66, .row .column.column-offset-67 {
|
||||||
|
margin-left: 66.6666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-75 {
|
||||||
|
margin-left: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-80 {
|
||||||
|
margin-left: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-90 {
|
||||||
|
margin-left: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-10 {
|
||||||
|
flex: 0 0 10%;
|
||||||
|
max-width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-20 {
|
||||||
|
flex: 0 0 20%;
|
||||||
|
max-width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-25 {
|
||||||
|
flex: 0 0 25%;
|
||||||
|
max-width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-33, .row .column.column-34 {
|
||||||
|
flex: 0 0 33.3333%;
|
||||||
|
max-width: 33.3333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-40 {
|
||||||
|
flex: 0 0 40%;
|
||||||
|
max-width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-50 {
|
||||||
|
flex: 0 0 50%;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-60 {
|
||||||
|
flex: 0 0 60%;
|
||||||
|
max-width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-66, .row .column.column-67 {
|
||||||
|
flex: 0 0 66.6666%;
|
||||||
|
max-width: 66.6666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-75 {
|
||||||
|
flex: 0 0 75%;
|
||||||
|
max-width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-80 {
|
||||||
|
flex: 0 0 80%;
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-90 {
|
||||||
|
flex: 0 0 90%;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column .column-top {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column .column-bottom {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column .column-center {
|
||||||
|
-ms-grid-row-align: center;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 40rem) {
|
||||||
|
.row {
|
||||||
|
flex-direction: row;
|
||||||
|
margin-left: -1.0rem;
|
||||||
|
width: calc(100% + 2.0rem);
|
||||||
|
}
|
||||||
|
.row .column {
|
||||||
|
margin-bottom: inherit;
|
||||||
|
padding: 0 1.0rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #9b4dca;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:focus, a:hover {
|
||||||
|
color: #606c76;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl,
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin-top: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl dl,
|
||||||
|
dl ol,
|
||||||
|
dl ul,
|
||||||
|
ol dl,
|
||||||
|
ol ol,
|
||||||
|
ol ul,
|
||||||
|
ul dl,
|
||||||
|
ul ol,
|
||||||
|
ul ul {
|
||||||
|
font-size: 90%;
|
||||||
|
margin: 1.5rem 0 1.5rem 3.0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
list-style: decimal inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: circle inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button,
|
||||||
|
button,
|
||||||
|
dd,
|
||||||
|
dt,
|
||||||
|
li {
|
||||||
|
margin-bottom: 1.0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote,
|
||||||
|
dl,
|
||||||
|
figure,
|
||||||
|
form,
|
||||||
|
ol,
|
||||||
|
p,
|
||||||
|
pre,
|
||||||
|
table,
|
||||||
|
ul {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-spacing: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border-bottom: 0.1rem solid #e1e1e1;
|
||||||
|
padding: 1.2rem 1.5rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:first-child,
|
||||||
|
th:first-child {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:last-child,
|
||||||
|
th:last-child {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: -.1rem;
|
||||||
|
margin-bottom: 2.0rem;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 4.6rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 3.6rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 2.8rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
letter-spacing: -.08rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
letter-spacing: -.05rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearfix:after {
|
||||||
|
clear: both;
|
||||||
|
content: ' ';
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-left {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*# sourceMappingURL=milligram.css.map */
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||||
|
|
||||||
|
/* Document
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the line height in all browsers.
|
||||||
|
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
line-height: 1.15; /* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the margin in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the `main` element consistently in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the font size and margin on `h1` elements within `section` and
|
||||||
|
* `article` contexts in Chrome, Firefox, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin: 0.67em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grouping content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in Firefox.
|
||||||
|
* 2. Show the overflow in Edge and IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
box-sizing: content-box; /* 1 */
|
||||||
|
height: 0; /* 1 */
|
||||||
|
overflow: visible; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text-level semantics
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the gray background on active links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove the bottom border in Chrome 57-
|
||||||
|
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
border-bottom: none; /* 1 */
|
||||||
|
text-decoration: underline; /* 2 */
|
||||||
|
text-decoration: underline dotted; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||||
|
* all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embedded content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the border on images inside links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Change the font styles in all browsers.
|
||||||
|
* 2. Remove the margin in Firefox and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit; /* 1 */
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
line-height: 1.15; /* 1 */
|
||||||
|
margin: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the overflow in IE.
|
||||||
|
* 1. Show the overflow in Edge.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input { /* 1 */
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||||
|
* 1. Remove the inheritance of text transform in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select { /* 1 */
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type="button"],
|
||||||
|
[type="reset"],
|
||||||
|
[type="submit"] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner border and padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
[type="button"]::-moz-focus-inner,
|
||||||
|
[type="reset"]::-moz-focus-inner,
|
||||||
|
[type="submit"]::-moz-focus-inner {
|
||||||
|
border-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the focus styles unset by the previous rule.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button:-moz-focusring,
|
||||||
|
[type="button"]:-moz-focusring,
|
||||||
|
[type="reset"]:-moz-focusring,
|
||||||
|
[type="submit"]:-moz-focusring {
|
||||||
|
outline: 1px dotted ButtonText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
padding: 0.35em 0.75em 0.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the text wrapping in Edge and IE.
|
||||||
|
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||||
|
* 3. Remove the padding so developers are not caught out when they zero out
|
||||||
|
* `fieldset` elements in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
color: inherit; /* 2 */
|
||||||
|
display: table; /* 1 */
|
||||||
|
max-width: 100%; /* 1 */
|
||||||
|
padding: 0; /* 3 */
|
||||||
|
white-space: normal; /* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the default vertical scrollbar in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in IE 10.
|
||||||
|
* 2. Remove the padding in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="checkbox"],
|
||||||
|
[type="radio"] {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="number"]::-webkit-inner-spin-button,
|
||||||
|
[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
* 2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
outline-offset: -2px; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner padding in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
* 2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button; /* 1 */
|
||||||
|
font: inherit; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interactive
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
details {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Misc
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
import { mount, Component, useState, loadFile } from "@odoo/owl";
|
||||||
|
|
||||||
|
class Counter extends Component {
|
||||||
|
static template = "Counter";
|
||||||
|
setup() {
|
||||||
|
this.state = useState({ value: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const templates = await loadFile("./counter.xml");
|
||||||
|
|
||||||
|
mount(Counter, document.getElementById("app-container"), { templates });
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<templates>
|
||||||
|
<t t-name="Counter">
|
||||||
|
<button t-on-click="increment">
|
||||||
|
Click Me! [<t t-esc="state.value"/>]
|
||||||
|
</button>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { loadFile } from "@odoo/owl";
|
||||||
|
|
||||||
|
for (const [className, type] of [["xml", "xml"], ["javascript", "js"]]) {
|
||||||
|
loadFile(`./counter.${type}`).then(code => {
|
||||||
|
const el = document.querySelector(`code.${className}`);
|
||||||
|
el.textContent = code;
|
||||||
|
hljs.highlightBlock(el);
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>OWL Framework</title>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
|
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
|
||||||
|
<link rel="stylesheet" href="assets/normalize.css">
|
||||||
|
<link rel="stylesheet" href="assets/milligram.css">
|
||||||
|
<link rel="stylesheet" href="assets/highlight.tomorrow.css">
|
||||||
|
<link rel="stylesheet" href="assets/main.css">
|
||||||
|
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="container">
|
||||||
|
<img class="homepage-logo" src="assets/owl_1f989.png" alt="OWL Logo">
|
||||||
|
<hgroup>
|
||||||
|
<h1>OWL Framework</h1>
|
||||||
|
<p>Mysterious OWL: A web framework for structured, dynamic and maintainable applications</p>
|
||||||
|
</hgroup>
|
||||||
|
</header>
|
||||||
|
<nav class="container">
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/odoo/owl" title="https://github.com/odoo/owl">Github</a>
|
||||||
|
•
|
||||||
|
<a href="https://github.com/odoo/owl/blob/master/doc/readme.md">Documentation</a>
|
||||||
|
•
|
||||||
|
<a href="playground/">Playground</a>
|
||||||
|
</p>
|
||||||
|
</nav>
|
||||||
|
<main>
|
||||||
|
<article class="design">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Why OWL?</h2>
|
||||||
|
<div class="row">
|
||||||
|
<section class="column">
|
||||||
|
<h3>XML based</h3>
|
||||||
|
<p>Templates are based on the XML format, which allows interesting applications. For example, they could be stored in a database and modified dynamically with <tt>xpaths</tt>.</p>
|
||||||
|
</section>
|
||||||
|
<section class="column">
|
||||||
|
<h3>Templates compiled in the browser</h3>
|
||||||
|
<p>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.</p>
|
||||||
|
</section>
|
||||||
|
<section class="column">
|
||||||
|
<h3>No toolchain required</h3>
|
||||||
|
<p>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 <tt>npm</tt>.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="example">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Example</h2>
|
||||||
|
<div class="row">
|
||||||
|
<section class="column">
|
||||||
|
<pre><code class="javascript"></code></pre>
|
||||||
|
</section>
|
||||||
|
<section class="column">
|
||||||
|
<pre><code class="xml"></code></pre>
|
||||||
|
<div id="app-container"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<p><a href=".">OWL</a> is licensed under LGPLv3.<br>Logo from <a href="https://github.com/googlefonts/noto-emoji">Google Noto Emoji Font</a>, licensed under Apache License 2.0</p>
|
||||||
|
</footer>
|
||||||
|
<script src="assets/highlight.pack.js"></script>
|
||||||
|
<script type="module" src="display_code.js"></script>
|
||||||
|
<script type="module" src="counter.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>OWL Playground</title>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
|
|
||||||
|
<script src="libs/ace.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/solid.css" integrity="sha384-QokYePQSOwpBDuhlHOsX0ymF6R/vLk/UQVz3WHa6wygxI5oGTmDTv8wahFOSspdm" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/fontawesome.css" integrity="sha384-vd1e11sR28tEK9YANUtpIOdjGW14pS87bUBuOIoBILVWLFnS+MCX9T6MMf0VdPGq" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<!-- Application JS/CSS -->
|
||||||
|
<link rel="stylesheet" href="playground.css">
|
||||||
|
<script type="module" src="playground.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -20,7 +20,7 @@ body {
|
|||||||
display: grid;
|
display: grid;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
grid-template-columns: auto 8px 100%;
|
grid-template-columns: auto 8px 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* LEFT BAR ****************************************/
|
/* LEFT BAR ****************************************/
|
||||||
@@ -1,7 +1,19 @@
|
|||||||
import { SAMPLES } from "./samples.js";
|
|
||||||
import { debounce, loadJS } from "./utils.js";
|
import { debounce, loadJS } from "./utils.js";
|
||||||
import { exportStandaloneApp } from "./export_snippet.js";
|
import {
|
||||||
const { useState, useRef, onMounted, onWillUnmount, onPatched, onWillUpdateProps } = owl;
|
App,
|
||||||
|
Component,
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
onMounted,
|
||||||
|
onWillUnmount,
|
||||||
|
onPatched,
|
||||||
|
onWillUpdateProps,
|
||||||
|
loadFile as _loadFile,
|
||||||
|
whenReady,
|
||||||
|
__info__,
|
||||||
|
useEffect,
|
||||||
|
onWillStart,
|
||||||
|
} from "../owl.js";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Constants, helpers, utils
|
// Constants, helpers, utils
|
||||||
@@ -16,40 +28,37 @@ const MODES = {
|
|||||||
const DEFAULT_XML = `<templates>
|
const DEFAULT_XML = `<templates>
|
||||||
</templates>`;
|
</templates>`;
|
||||||
|
|
||||||
|
// Memoize loadFile so that samples aren't reloaded whenever a sample is selected
|
||||||
|
const fileCache = {};
|
||||||
|
const loadFile = (path) => {
|
||||||
|
if (!(path in fileCache)) {
|
||||||
|
fileCache[path] = _loadFile(path);
|
||||||
|
}
|
||||||
|
return fileCache[path];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make an iframe, with all the js, css and xml properly injected.
|
* Make an iframe, with all the js, css and xml properly injected.
|
||||||
*/
|
*/
|
||||||
function makeCodeIframe(js, css, xml) {
|
function makeCodeIframe(js, css, xml) {
|
||||||
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "").replace(/`/g, '\\\`');
|
|
||||||
|
|
||||||
// create iframe
|
|
||||||
const iframe = document.createElement("iframe");
|
const iframe = document.createElement("iframe");
|
||||||
|
|
||||||
iframe.onload = () => {
|
iframe.onload = () => {
|
||||||
const doc = iframe.contentDocument;
|
const doc = iframe.contentDocument;
|
||||||
const utilsMod = doc.createElement("script");
|
|
||||||
utilsMod.setAttribute("type", "module");
|
|
||||||
utilsMod.setAttribute("src", "utils.js");
|
|
||||||
doc.head.appendChild(utilsMod);
|
|
||||||
// inject js
|
|
||||||
const owlScript = doc.createElement("script");
|
|
||||||
owlScript.type = "text/javascript";
|
|
||||||
owlScript.src = "../owl.js";
|
|
||||||
owlScript.addEventListener("load", () => {
|
|
||||||
const script = doc.createElement("script");
|
|
||||||
script.type = "module";
|
|
||||||
const content = `
|
|
||||||
(async function(TEMPLATES) {
|
|
||||||
${js}
|
|
||||||
})(\`${sanitizedXML}\`)`;
|
|
||||||
script.innerHTML = content;
|
|
||||||
doc.body.appendChild(script);
|
|
||||||
});
|
|
||||||
doc.head.appendChild(owlScript);
|
|
||||||
|
|
||||||
// inject css
|
const importMap = doc.createElement("script");
|
||||||
|
importMap.type = "importmap";
|
||||||
|
importMap.textContent = `{ "imports": { "@odoo/owl": "../owl.js" } }`;
|
||||||
|
doc.head.appendChild(importMap);
|
||||||
|
|
||||||
|
const script = doc.createElement("script");
|
||||||
|
script.type = "module";
|
||||||
|
// escape characters with special meaning in template literals
|
||||||
|
const escapedXml = xml.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/, "\\${");
|
||||||
|
script.textContent = `const TEMPLATES = \`${escapedXml}\`\n${js}`;
|
||||||
|
doc.body.appendChild(script);
|
||||||
|
|
||||||
const style = document.createElement("style");
|
const style = document.createElement("style");
|
||||||
style.innerHTML = css;
|
style.innerText = css;
|
||||||
doc.head.appendChild(style);
|
doc.head.appendChild(style);
|
||||||
};
|
};
|
||||||
return iframe;
|
return iframe;
|
||||||
@@ -58,53 +67,84 @@ function makeCodeIframe(js, css, xml) {
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// SAMPLES
|
// SAMPLES
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const SAMPLES = [
|
||||||
|
{
|
||||||
|
description: "Components",
|
||||||
|
folder: "components",
|
||||||
|
code: ["js", "xml", "css"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Form Input Bindings",
|
||||||
|
folder: "form",
|
||||||
|
code: ["js", "xml"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Inline templates",
|
||||||
|
folder: "single_file_component",
|
||||||
|
code: ["js"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Lifecycle demo",
|
||||||
|
folder: "lifecycle_demo",
|
||||||
|
code: ["js", "xml", "css"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Customized hook",
|
||||||
|
folder: "custom_hooks",
|
||||||
|
code: ["js", "xml", "css"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Todo List App (with reactivity)",
|
||||||
|
folder: "todo_app",
|
||||||
|
code: ["js", "xml", "css"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Responsive app",
|
||||||
|
folder: "responsive_app",
|
||||||
|
code: ["js", "xml", "css"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Slots And Generic Components",
|
||||||
|
folder: "slots",
|
||||||
|
code: ["js", "xml", "css"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Window Management System",
|
||||||
|
folder: "window_manager",
|
||||||
|
code: ["js", "xml", "css"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Benchmark example",
|
||||||
|
folder: "benchmark",
|
||||||
|
code: ["js", "xml", "css"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
function loadSamples() {
|
function loadSamples() {
|
||||||
let result = SAMPLES.slice();
|
const result = SAMPLES.map(({ description, folder, code }) => ({
|
||||||
|
description,
|
||||||
|
code: async () => Object.fromEntries(
|
||||||
|
await Promise.all(
|
||||||
|
code.map(async (type) => [type, await loadFile(`./samples/${folder}/${folder}.${type}`)])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}));
|
||||||
const localSample = localStorage.getItem("owl-playground-local-sample");
|
const localSample = localStorage.getItem("owl-playground-local-sample");
|
||||||
if (localSample) {
|
if (localSample) {
|
||||||
const { js, css, xml } = JSON.parse(localSample);
|
const { js, css, xml } = JSON.parse(localSample);
|
||||||
result.unshift({
|
result.unshift({
|
||||||
description: "Local Storage Code",
|
description: "Local Storage Code",
|
||||||
code: js,
|
code: () => Promise.resolve({ js, xml, css }),
|
||||||
xml,
|
|
||||||
css
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveLocalSample(js, css, xml) {
|
|
||||||
const str = JSON.stringify({ js, css, xml });
|
|
||||||
localStorage.setItem("owl-playground-local-sample", str);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteLocalSample() {
|
|
||||||
localStorage.removeItem("owl-playground-local-sample");
|
|
||||||
}
|
|
||||||
|
|
||||||
function useSamples() {
|
|
||||||
const samples = loadSamples();
|
|
||||||
const component = owl.useComponent();
|
|
||||||
let interval;
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const state = component.state;
|
|
||||||
interval = setInterval(() => {
|
|
||||||
if (component.isDirty) {
|
|
||||||
saveLocalSample(state.js, state.css, state.xml);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
onWillUnmount(() => {
|
|
||||||
clearInterval(interval);
|
|
||||||
});
|
|
||||||
return samples;
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Tabbed editor
|
// Tabbed editor
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
class TabbedEditor extends owl.Component {
|
class TabbedEditor extends Component {
|
||||||
setup() {
|
setup() {
|
||||||
const props = this.props;
|
const props = this.props;
|
||||||
this.state = useState({
|
this.state = useState({
|
||||||
@@ -168,7 +208,7 @@ class TabbedEditor extends owl.Component {
|
|||||||
const resizer = ev => {
|
const resizer = ev => {
|
||||||
const delta = ev.clientY - y;
|
const delta = ev.clientY - y;
|
||||||
y = ev.clientY;
|
y = ev.clientY;
|
||||||
this.trigger("updatePanelHeight", { delta });
|
this.props.updatePanelHeight({ delta });
|
||||||
};
|
};
|
||||||
document.body.addEventListener("mousemove", resizer);
|
document.body.addEventListener("mousemove", resizer);
|
||||||
document.body.addEventListener("mouseup", () => {
|
document.body.addEventListener("mouseup", () => {
|
||||||
@@ -205,36 +245,53 @@ TabbedEditor.template = "TabbedEditor";
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// MAIN APP
|
// MAIN APP
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
class App extends owl.Component {
|
class Playground extends Component {
|
||||||
|
static template = "Playground";
|
||||||
|
static components = { TabbedEditor };
|
||||||
setup() {
|
setup() {
|
||||||
this.version = owl.__info__.version;
|
this.version = __info__.version;
|
||||||
this.SAMPLES = useSamples();
|
|
||||||
this.isDirty = false;
|
|
||||||
|
|
||||||
|
this.isDirty = false;
|
||||||
this.state = useState({
|
this.state = useState({
|
||||||
js: this.SAMPLES[0].code,
|
js: "",
|
||||||
css: this.SAMPLES[0].css || "",
|
css: "",
|
||||||
xml: this.SAMPLES[0].xml || DEFAULT_XML,
|
xml: DEFAULT_XML,
|
||||||
displayWelcome: true,
|
displayWelcome: true,
|
||||||
splitLayout: true,
|
splitLayout: true,
|
||||||
leftPaneWidth: Math.ceil(window.innerWidth / 2),
|
leftPaneWidth: Math.ceil(window.innerWidth / 2),
|
||||||
topPanelHeight: null
|
topPanelHeight: null
|
||||||
});
|
});
|
||||||
|
|
||||||
this.toggleLayout = debounce(this.toggleLayout, 250, true);
|
this.samples = loadSamples();
|
||||||
this.runCode = debounce(this.runCode, 250, true);
|
|
||||||
this.downloadCode = debounce(this.downloadCode, 250, true);
|
|
||||||
this.content = useRef("content");
|
|
||||||
this.updateCode = this.updateCode.bind(this);
|
|
||||||
if (window.location.hash) {
|
if (window.location.hash) {
|
||||||
try {
|
try {
|
||||||
const { js, css, xml } = JSON.parse(atob(decodeURIComponent(window.location.hash.slice(1))));
|
const { js, css, xml } = JSON.parse(atob(decodeURIComponent(window.location.hash.slice(1))));
|
||||||
if (![js, css, xml].every(item => typeof item === "string")) {
|
if ([js, css, xml].every(item => typeof item === "string")) {
|
||||||
return;
|
Object.assign(this.state, { js, css, xml });
|
||||||
}
|
}
|
||||||
Object.assign(this.state, { js, css, xml });
|
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
onWillStart(async () => {
|
||||||
|
if (!this.state.js) {
|
||||||
|
this.setSample(await this.samples[0].code());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (this.isDirty) {
|
||||||
|
const { js, css, xml } = this.state;
|
||||||
|
const str = JSON.stringify({ js, css, xml });
|
||||||
|
localStorage.setItem("owl-playground-local-sample", str);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, () => []);
|
||||||
|
|
||||||
|
this.toggleLayout = debounce(this.toggleLayout, 250, true);
|
||||||
|
this.runCode = debounce(this.runCode, 250, true);
|
||||||
|
this.exportStandaloneApp = debounce(this.exportStandaloneApp, 250, true);
|
||||||
|
this.content = useRef("content");
|
||||||
|
this.updateCode = this.updateCode.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
runCode() {
|
runCode() {
|
||||||
@@ -258,12 +315,11 @@ class App extends owl.Component {
|
|||||||
window.location.href = link.href;
|
window.location.href = link.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSample(ev) {
|
setSample(sample) {
|
||||||
const sample = this.SAMPLES.find(s => s.description === ev.target.value);
|
this.state.js = sample.js;
|
||||||
this.state.js = sample.code;
|
|
||||||
this.state.css = sample.css || "";
|
this.state.css = sample.css || "";
|
||||||
this.state.xml = sample.xml || DEFAULT_XML;
|
this.state.xml = sample.xml || DEFAULT_XML;
|
||||||
deleteLocalSample();
|
localStorage.removeItem("owl-playground-local-sample");
|
||||||
this.isDirty = false;
|
this.isDirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,14 +327,14 @@ class App extends owl.Component {
|
|||||||
return `width:${this.state.leftPaneWidth}px`;
|
return `width:${this.state.leftPaneWidth}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get rightPaneStyle() {
|
|
||||||
return `width:${window.innerWidth - 6 - this.state.leftPaneWidth}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
get topEditorStyle() {
|
get topEditorStyle() {
|
||||||
return `flex: 0 0 ${this.state.topPanelHeight}px`;
|
return `flex: 0 0 ${this.state.topPanelHeight}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onSampleChange(ev) {
|
||||||
|
this.setSample(await this.samples.find(s => s.description === ev.target.value).code());
|
||||||
|
}
|
||||||
|
|
||||||
onMouseDown() {
|
onMouseDown() {
|
||||||
const resizer = ev => {
|
const resizer = ev => {
|
||||||
this.state.leftPaneWidth = ev.clientX;
|
this.state.leftPaneWidth = ev.clientX;
|
||||||
@@ -305,39 +361,40 @@ class App extends owl.Component {
|
|||||||
toggleLayout() {
|
toggleLayout() {
|
||||||
this.state.splitLayout = !this.state.splitLayout;
|
this.state.splitLayout = !this.state.splitLayout;
|
||||||
}
|
}
|
||||||
updatePanelHeight(ev) {
|
updatePanelHeight({ delta }) {
|
||||||
if (!ev.detail.delta) {
|
let height = this.state.topPanelHeight || document.querySelector(".tabbed-editor").clientHeight;
|
||||||
return;
|
this.state.topPanelHeight = height + delta;
|
||||||
}
|
|
||||||
let height = this.state.topPanelHeight;
|
|
||||||
if (!height) {
|
|
||||||
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
|
|
||||||
}
|
|
||||||
this.state.topPanelHeight = height + ev.detail.delta;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadCode() {
|
async exportStandaloneApp() {
|
||||||
const { js, css, xml } = this.state;
|
const { js, css, xml } = this.state;
|
||||||
const content = await exportStandaloneApp(js, css, xml);
|
|
||||||
|
await loadJS("libs/jszip.min.js");
|
||||||
|
const zip = new JSZip();
|
||||||
|
zip.file("app.py", await loadFile("./standalone_app/app.py"));
|
||||||
|
zip.file("index.html", await loadFile("./standalone_app/index.html"));
|
||||||
|
zip.file("owl.js", await loadFile("../owl.js"));
|
||||||
|
zip.file("app.js", `const TEMPLATES = await (await fetch('app.xml')).text();\n${js}`);
|
||||||
|
zip.file("app.css", css);
|
||||||
|
zip.file("app.xml", xml);
|
||||||
|
|
||||||
await loadJS("libs/FileSaver.min.js");
|
await loadJS("libs/FileSaver.min.js");
|
||||||
saveAs(content, "app.zip");
|
saveAs(await zip.generateAsync({ type: "blob" }), "app.zip");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
App.components = { TabbedEditor };
|
|
||||||
App.template = "App";
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Application initialization
|
// Application initialization
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
async function start() {
|
async function start() {
|
||||||
document.title = `${document.title} (v${owl.__info__.version})`;
|
document.title = `${document.title} (v${__info__.version})`;
|
||||||
const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`;
|
const commit = `https://github.com/odoo/owl/commit/${__info__.hash}`;
|
||||||
console.info(`This application is using Owl built with the following commit:`, commit);
|
console.info(`This application is using Owl built with the following commit:`, commit);
|
||||||
const [templates] = await Promise.all([
|
const [templates] = await Promise.all([
|
||||||
owl.loadFile("templates.xml"),
|
loadFile("templates.xml"),
|
||||||
owl.whenReady()
|
whenReady()
|
||||||
]);
|
]);
|
||||||
const rootApp = new owl.App(App);
|
const rootApp = new App(Playground, { name: "Owl Playground" });
|
||||||
rootApp.addTemplates(templates);
|
rootApp.addTemplates(templates);
|
||||||
|
|
||||||
await rootApp.mount(document.body);
|
await rootApp.mount(document.body);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
tr.danger {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove:hover {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.remove {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { Component, mount, useState, onPatched} from "@odoo/owl";
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Data generation
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let idCounter = 1;
|
||||||
|
const adjectives = [
|
||||||
|
"pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain",
|
||||||
|
"quaint", "clean", "elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd",
|
||||||
|
"unsightly", "adorable", "important", "inexpensive", "cheap", "expensive", "fancy"];
|
||||||
|
const colours = ["red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange"];
|
||||||
|
const nouns = ["table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse", "keyboard"];
|
||||||
|
|
||||||
|
function _random (max) { return Math.round(Math.random() * 1000) % max; };
|
||||||
|
|
||||||
|
function buildData(count) {
|
||||||
|
const data = new Array(count);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const label = `${adjectives[_random(adjectives.length)]} ${colours[_random(colours.length)]} ${nouns[_random(nouns.length)]}`;
|
||||||
|
data[i] = {
|
||||||
|
id: idCounter++,
|
||||||
|
label,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Components
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
class Button extends Component {
|
||||||
|
static template = "Button";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Row extends Component {
|
||||||
|
static template = "Row";
|
||||||
|
}
|
||||||
|
|
||||||
|
class Root extends Component {
|
||||||
|
static template = "Root";
|
||||||
|
static components = { Button, Row };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.state = useState({
|
||||||
|
rows: [],
|
||||||
|
selectedRowId: null
|
||||||
|
});
|
||||||
|
this.benchmarking = false;
|
||||||
|
onPatched(() => {
|
||||||
|
if (this.benchmarking) {
|
||||||
|
this.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
start(descr) {
|
||||||
|
this.benchmarking = `[${descr}]`;
|
||||||
|
console.time(this.benchmarking);
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
console.timeEnd(this.benchmarking);
|
||||||
|
this.benchmarking = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
this.start('add1000');
|
||||||
|
this.state.rows = buildData(1000);
|
||||||
|
this.state.selectedRowId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
runLots() {
|
||||||
|
this.start('add10_000');
|
||||||
|
this.state.rows = buildData(10_000);
|
||||||
|
this.state.selectedRowId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
add() {
|
||||||
|
this.start('append1000');
|
||||||
|
this.state.rows = this.state.rows.concat(buildData(1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.start('update1/10th');
|
||||||
|
let index = 0;
|
||||||
|
const rows = this.state.rows;
|
||||||
|
while (index < rows.length) {
|
||||||
|
rows[index].label = rows[index].label + " !!!";
|
||||||
|
index += 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.start('clear');
|
||||||
|
this.state.rows = [];
|
||||||
|
this.state.selectedRowId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
swapRows() {
|
||||||
|
this.start('swap');
|
||||||
|
const rows = this.state.rows;
|
||||||
|
if (rows.length > 998) {
|
||||||
|
let tmp = rows[1];
|
||||||
|
rows[1] = rows[998];
|
||||||
|
rows[998] = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectRow(id) {
|
||||||
|
this.start('select');
|
||||||
|
this.state.selectedRowId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRow(id) {
|
||||||
|
this.start('remove1');
|
||||||
|
const rows = this.state.rows;
|
||||||
|
rows.splice(rows.findIndex(row => row.id === id), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// dev=false for benchmarking. we don't want to benchmark dev code!
|
||||||
|
mount(Root, document.body, { templates: TEMPLATES, dev: false });
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<templates>
|
||||||
|
<t t-name="Button">
|
||||||
|
<div class='col-sm-6 smallpad'>
|
||||||
|
<button t-att-id="props.id" class='btn btn-primary btn-block' type='button' t-on-click="props.onClick">
|
||||||
|
<t t-esc="props.text"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-name="Row">
|
||||||
|
<tr t-att-class="props.isSelected ? 'danger' : ''">
|
||||||
|
<td class="col-md-1" t-esc="props.row.id" />
|
||||||
|
<td class="col-md-4">
|
||||||
|
<a t-on-click="() => props.onSelect(props.row.id)" t-esc="props.row.label" />
|
||||||
|
</td>
|
||||||
|
<td class="col-md-1">
|
||||||
|
<a t-on-click="() => props.onRemove(props.row.id)" class="remove">[x]
|
||||||
|
<span class='glyphicon glyphicon-remove' aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class='col-md-6'/>
|
||||||
|
</tr>
|
||||||
|
</t>
|
||||||
|
<t t-name="Root">
|
||||||
|
<div class='container'>
|
||||||
|
<div class='jumbotron'>
|
||||||
|
<div class='row'>
|
||||||
|
<div class='col-md-6'>
|
||||||
|
<h1>Owl Keyed</h1>
|
||||||
|
</div>
|
||||||
|
<div class='col-md-6'>
|
||||||
|
<div class='row'>
|
||||||
|
<Button id="'run'" onClick.bind="run" text="'Create 1,000 rows'" />
|
||||||
|
<Button id="'runlots'" onClick.bind="runLots" text="'Create 10,000 rows'" />
|
||||||
|
<Button id="'add'" onClick.bind="add" text="'Append 1,000 rows'" />
|
||||||
|
<Button id="'update'" onClick.bind="update" text="'Update every 10th row'" />
|
||||||
|
<Button id="'clear'" onClick.bind="clear" text="'Clear'" />
|
||||||
|
<Button id="'swaprows'" onClick.bind="swapRows" text="'Swap Rows'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table class='table table-hover table-striped test-data'>
|
||||||
|
<tbody>
|
||||||
|
<t t-foreach="state.rows" t-as="row" t-key="row.id">
|
||||||
|
<Row row="row" isSelected="row.id === state.selectedRowId" onSelect.bind="selectRow" onRemove.bind="removeRow"/>
|
||||||
|
</t>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<span class='preloadicon glyphicon glyphicon-remove' aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.greeter {
|
||||||
|
font-size: 20px;
|
||||||
|
width: 300px;
|
||||||
|
height: 100px;
|
||||||
|
margin: 5px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 100px;
|
||||||
|
background-color: #eeeeee;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// In this example, we show how components can be defined and created.
|
||||||
|
import { Component, useState, mount } from "@odoo/owl";
|
||||||
|
|
||||||
|
class Greeter extends Component {
|
||||||
|
static template = "Greeter";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.state = useState({ word: 'Hello' });
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main root component
|
||||||
|
class Root extends Component {
|
||||||
|
static components = { Greeter };
|
||||||
|
static template = "Root"
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.state = useState({ name: 'World'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(Root, document.body, { templates: TEMPLATES, dev: true });
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<templates>
|
||||||
|
<div t-name="Greeter" class="greeter" t-on-click="toggle">
|
||||||
|
<t t-esc="state.word"/>, <t t-esc="props.name"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<t t-name="Root">
|
||||||
|
<Greeter name="state.name"/>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
button {
|
||||||
|
width: 120px;
|
||||||
|
height: 35px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// In this example, we show how hooks can be used or defined.
|
||||||
|
import { Component, mount, useState, onWillDestroy } from "@odoo/owl";
|
||||||
|
|
||||||
|
// We define here a custom behaviour: this hook tracks the state of the mouse
|
||||||
|
// position
|
||||||
|
function useMouse() {
|
||||||
|
const position = useState({x:0, y: 0});
|
||||||
|
|
||||||
|
function update(e) {
|
||||||
|
position.x = e.clientX;
|
||||||
|
position.y = e.clientY;
|
||||||
|
}
|
||||||
|
window.addEventListener('mousemove', update);
|
||||||
|
onWillDestroy(() => {
|
||||||
|
window.removeEventListener('mousemove', update);
|
||||||
|
});
|
||||||
|
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Main root component
|
||||||
|
class Root extends Component {
|
||||||
|
static template = "Root";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
// simple state hook (reactive object)
|
||||||
|
this.counter = useState({ value: 0 });
|
||||||
|
|
||||||
|
// this hooks is bound to the 'mouse' property.
|
||||||
|
this.mouse = useMouse();
|
||||||
|
}
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.counter.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application setup
|
||||||
|
mount(Root, document.body, { templates: TEMPLATES, dev: true });
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
<templates>
|
||||||
|
<div t-name="Root">
|
||||||
|
<button t-on-click="increment">Click! <t t-esc="counter.value"/></button>
|
||||||
|
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// This example illustrate how the t-model directive can be used to synchronize
|
||||||
|
// data between html inputs (and select/textareas) and the state of a component.
|
||||||
|
// Note that there are two controls with t-model="color": they are totally
|
||||||
|
// synchronized.
|
||||||
|
import { Component, useState, mount } from "@odoo/owl";
|
||||||
|
|
||||||
|
class Form extends Component {
|
||||||
|
static template = "Form";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.state = useState({
|
||||||
|
text: "",
|
||||||
|
othertext: "",
|
||||||
|
number: 11,
|
||||||
|
color: "",
|
||||||
|
bool: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application setup
|
||||||
|
mount(Form, document.body, { templates: TEMPLATES, dev: true });
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<templates>
|
||||||
|
<div t-name="Form">
|
||||||
|
<h1>Form</h1>
|
||||||
|
<div>
|
||||||
|
Text (immediate): <input t-model="state.text"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Other text (lazy): <input t-model.lazy="state.othertext"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Number: <input t-model.number="state.number"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Boolean: <input type="checkbox" t-model="state.bool"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Color, with a select:
|
||||||
|
<select t-model="state.color">
|
||||||
|
<option value="">Select a color</option>
|
||||||
|
<option value="red">Red</option>
|
||||||
|
<option value="blue">Blue</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Color, with radio buttons:
|
||||||
|
<span><input type="radio" name="color" id="red" value="red" t-model="state.color"/><label for="red">Red</label></span>
|
||||||
|
<span><input type="radio" name="color" id="blue" value="blue" t-model="state.color"/><label for="blue">Blue</label></span>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<h1>State</h1>
|
||||||
|
<div>Text: <t t-esc="state.text"/></div>
|
||||||
|
<div>Other Text: <t t-esc="state.othertext"/></div>
|
||||||
|
<div>Number: <t t-esc="state.number"/></div>
|
||||||
|
<div>Boolean: <t t-if="state.bool">True</t><t t-else="">False</t></div>
|
||||||
|
<div>Color: <t t-esc="state.color"/></div>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"owl": ["../../../src/index.ts"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
button {
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo {
|
||||||
|
margin: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #dddddd;
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// This example shows all the possible lifecycle hooks
|
||||||
|
//
|
||||||
|
// The root component controls a sub component (DemoComponent). It logs all its lifecycle
|
||||||
|
// methods in the console. Try modifying its state by clicking on it, or by
|
||||||
|
// clicking on the two main buttons, and look into the console to see what
|
||||||
|
// happens.
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
useState,
|
||||||
|
mount,
|
||||||
|
useComponent,
|
||||||
|
onWillStart,
|
||||||
|
onMounted,
|
||||||
|
onWillUnmount,
|
||||||
|
onWillUpdateProps,
|
||||||
|
onPatched,
|
||||||
|
onWillPatch,
|
||||||
|
onWillRender,
|
||||||
|
onRendered,
|
||||||
|
onWillDestroy,
|
||||||
|
} from "@odoo/owl";
|
||||||
|
|
||||||
|
function useLogLifecycle() {
|
||||||
|
const component = useComponent();
|
||||||
|
const name = component.constructor.name;
|
||||||
|
onWillStart(() => console.log(`${name}:willStart`));
|
||||||
|
onMounted(() => console.log(`${name}:mounted`));
|
||||||
|
onWillUpdateProps(() => console.log(`${name}:willUpdateProps`));
|
||||||
|
onWillRender(() => console.log(`${name}:willRender`));
|
||||||
|
onRendered(() => console.log(`${name}:rendered`));
|
||||||
|
onWillPatch(() => console.log(`${name}:willPatch`));
|
||||||
|
onPatched(() => console.log(`${name}:patched`));
|
||||||
|
onWillUnmount(() => console.log(`${name}:willUnmount`));
|
||||||
|
onWillDestroy(() => console.log(`${name}:willDestroy`));
|
||||||
|
}
|
||||||
|
|
||||||
|
class DemoComponent extends Component {
|
||||||
|
static template = "DemoComponent";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
this.state = useState({ n: 0 });
|
||||||
|
}
|
||||||
|
increment() {
|
||||||
|
this.state.n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Root extends Component {
|
||||||
|
static template = "Root";
|
||||||
|
static components = { DemoComponent };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
this.state = useState({ n: 0, flag: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.n++;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSubComponent() {
|
||||||
|
this.state.flag = !this.state.flag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(Root, document.body, { templates: TEMPLATES, dev: true });
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<templates>
|
||||||
|
<div t-name="DemoComponent" t-on-click="increment" class="demo">
|
||||||
|
<div>Demo Sub Component</div>
|
||||||
|
<div>(click on me to update me)</div>
|
||||||
|
<div>Props: <t t-esc="props.n"/>, State: <t t-esc="state.n"/>. </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="Root">
|
||||||
|
<button t-on-click="increment">Increment Parent State</button>
|
||||||
|
<button t-on-click="toggleSubComponent">Toggle SubComponent</button>
|
||||||
|
<div t-if="state.flag">
|
||||||
|
<DemoComponent n="state.n"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app.desktop {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
flex: 0 0 30px;
|
||||||
|
height: 30px;
|
||||||
|
background-color: cadetblue;
|
||||||
|
color: white;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlpanel {
|
||||||
|
flex: 0 0 100px;
|
||||||
|
height: 100px;
|
||||||
|
background-color: #dddddd;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formview {
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1 1 60%;
|
||||||
|
min-height: 200px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatter {
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1 1 40%;
|
||||||
|
background-color: #eeeeee;
|
||||||
|
color: #333333;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
// In this example, we show how one can design an application that is responsive:
|
||||||
|
// its UI is different in mobile mode or in desktop mode.
|
||||||
|
//
|
||||||
|
// The main idea is to have a "isMobile" key in the environment, then listen
|
||||||
|
// to resize events and update the env if needed. Then, the whole interface
|
||||||
|
// will be updated, creating and destroying components as needed.
|
||||||
|
//
|
||||||
|
// To see this in action, try resizing the window. The application will switch
|
||||||
|
// to mobile mode whenever it has less than 768px.
|
||||||
|
import { Component, useState, mount, reactive, useEnv } from "@odoo/owl";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function debounce(func, wait, immediate) {
|
||||||
|
let timeout;
|
||||||
|
return function () {
|
||||||
|
const context = this;
|
||||||
|
const args = arguments;
|
||||||
|
function later() {
|
||||||
|
timeout = null;
|
||||||
|
if (!immediate) {
|
||||||
|
func.apply(context, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const callNow = immediate && !timeout;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
if (callNow) {
|
||||||
|
func.apply(context, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Responsive hook
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function createUI() {
|
||||||
|
const getIsMobile = () => window.innerWidth <= 768;
|
||||||
|
|
||||||
|
const ui = reactive({ isMobile: getIsMobile() });
|
||||||
|
|
||||||
|
const updateEnv = debounce(() => {
|
||||||
|
const isMobile = getIsMobile();
|
||||||
|
if (ui.isMobile !== isMobile) {
|
||||||
|
ui.isMobile = isMobile;
|
||||||
|
}
|
||||||
|
}, 15);
|
||||||
|
window.addEventListener("resize", updateEnv);
|
||||||
|
return ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useUI() {
|
||||||
|
const env = useEnv();
|
||||||
|
return useState(env.ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Components
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
class Navbar extends Component {
|
||||||
|
static template = "Navbar";
|
||||||
|
}
|
||||||
|
|
||||||
|
class MobileSearchView extends Component {
|
||||||
|
static template = "MobileSearchView";
|
||||||
|
}
|
||||||
|
|
||||||
|
class ControlPanel extends Component {
|
||||||
|
static template = "ControlPanel";
|
||||||
|
static components = { MobileSearchView };
|
||||||
|
setup() {
|
||||||
|
this.ui = useUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AdvancedComponent extends Component {
|
||||||
|
static template = "AdvancedComponent";
|
||||||
|
}
|
||||||
|
|
||||||
|
class FormView extends Component {
|
||||||
|
static template = "FormView";
|
||||||
|
static components = { AdvancedComponent };
|
||||||
|
setup() {
|
||||||
|
this.ui = useUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Chatter extends Component {
|
||||||
|
static template = "Chatter";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.messages = Array.from(Array(100).keys());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Root extends Component {
|
||||||
|
static template = "Root";
|
||||||
|
static components = { Navbar, ControlPanel, FormView, Chatter };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.ui = useUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Application Startup
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
const env = {
|
||||||
|
ui: createUI()
|
||||||
|
};
|
||||||
|
|
||||||
|
mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<templates>
|
||||||
|
<div t-name="Navbar" class="navbar">Navbar</div>
|
||||||
|
|
||||||
|
<div t-name="ControlPanel" class="controlpanel">
|
||||||
|
<h2>Control Panel</h2>
|
||||||
|
<MobileSearchView t-if="ui.isMobile" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="FormView" class="formview">
|
||||||
|
<h2>Form View</h2>
|
||||||
|
<AdvancedComponent t-if="!ui.isMobile" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="Chatter" class="chatter">
|
||||||
|
<h2>Chatter</h2>
|
||||||
|
<t t-foreach="messages" t-as="item" t-key="item"><div>Message <t t-esc="item"/></div></t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="MobileSearchView">Mobile searchview</div>
|
||||||
|
|
||||||
|
<div t-name="AdvancedComponent">
|
||||||
|
This component is only created in desktop mode.
|
||||||
|
<button>Button!</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<t t-name="maincontent">
|
||||||
|
<FormView />
|
||||||
|
<Chatter />
|
||||||
|
</t>
|
||||||
|
<div t-name="Root" class="app" t-att-class="{mobile: ui.isMobile, desktop: !ui.isMobile}">
|
||||||
|
<Navbar/>
|
||||||
|
<ControlPanel/>
|
||||||
|
<div class="content-wrapper" t-if="!ui.isMobile">
|
||||||
|
<div class="content">
|
||||||
|
<t t-call="maincontent"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<t t-else="">
|
||||||
|
<t t-call="maincontent"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// This example illustrates how one can write Owl components with
|
||||||
|
// inline templates.
|
||||||
|
|
||||||
|
import { Component, useState, xml, mount } from "@odoo/owl";
|
||||||
|
|
||||||
|
// Counter component
|
||||||
|
class Counter extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<button t-on-click="() => state.value++">
|
||||||
|
Click! [<t t-esc="state.value"/>]
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
state = useState({ value: 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root
|
||||||
|
class Root extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<Counter/>
|
||||||
|
<Counter/>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
static components = { Counter };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application setup
|
||||||
|
mount(Root, document.body, { templates: TEMPLATES, dev: true});
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #eeeeee;
|
||||||
|
width: 200px;
|
||||||
|
height: 100px;
|
||||||
|
margin: 10px;
|
||||||
|
border: 1px solid gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.full {
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.small {
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
flex: 0 0 25px;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: darkcyan;
|
||||||
|
color: white;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title button {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
padding: 5px;
|
||||||
|
border-top: 1px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
border-top: 1px solid white;
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// We show here how slots can be used to create generic components.
|
||||||
|
// In this example, the Card component is basically only a container. It is not
|
||||||
|
// aware of its content. It just knows where it should be (with t-slot).
|
||||||
|
// The parent component define the content with t-set-slot.
|
||||||
|
//
|
||||||
|
// Note that the t-on-click event, defined in the Root template, is executed in
|
||||||
|
// the context of the Root component, even though it is inside the Card component
|
||||||
|
import { Component, useState, mount } from "@odoo/owl";
|
||||||
|
|
||||||
|
class Card extends Component {
|
||||||
|
static template = "Card";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.state = useState({ showContent: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDisplay() {
|
||||||
|
this.state.showContent = !this.state.showContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Counter extends Component {
|
||||||
|
static template = "Counter";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.state = useState({val: 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
inc() {
|
||||||
|
this.state.val++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main root component
|
||||||
|
class Root extends Component {
|
||||||
|
static template = "Root"
|
||||||
|
static components = { Card, Counter };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.state = useState({a: 1, b: 3});
|
||||||
|
}
|
||||||
|
|
||||||
|
inc(key, delta) {
|
||||||
|
this.state[key] += delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application setup
|
||||||
|
mount(Root, document.body, { templates: TEMPLATES, dev: true});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<templates>
|
||||||
|
<div t-name="Card" class="card" t-att-class="state.showContent ? 'full' : 'small'">
|
||||||
|
<div class="card-title">
|
||||||
|
<t t-esc="props.title"/><button t-on-click="toggleDisplay">Toggle</button>
|
||||||
|
</div>
|
||||||
|
<t t-if="state.showContent">
|
||||||
|
<div class="card-content" >
|
||||||
|
<t t-slot="content"/>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<t t-slot="footer"/>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="Counter">
|
||||||
|
<t t-esc="state.val"/><button t-on-click="inc">Inc</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="Root" class="main">
|
||||||
|
<Card title="'Title card A'">
|
||||||
|
<t t-set-slot="content">Content of card 1... [<t t-esc="state.a"/>]</t>
|
||||||
|
<t t-set-slot="footer"><button t-on-click="() => this.inc('a', 1)">Increment A</button></t>
|
||||||
|
</Card>
|
||||||
|
<Card title="'Title card B'">
|
||||||
|
<t t-set-slot="content">
|
||||||
|
<div>Card 2... [<t t-esc="state.b"/>]</div>
|
||||||
|
<Counter />
|
||||||
|
</t>
|
||||||
|
<t t-set-slot="footer"><button t-on-click="() => this.inc('b', -1)">Decrement B</button></t>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
html,body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
font-size: 100%;
|
||||||
|
vertical-align: baseline;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
color: inherit;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.4em;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #4d4d4d;
|
||||||
|
min-width: 230px;
|
||||||
|
max-width: 550px;
|
||||||
|
margin: 0 auto;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp {
|
||||||
|
background: #fff;
|
||||||
|
margin: 130px 0 40px 0;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp input::-webkit-input-placeholder {
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp input::-moz-placeholder {
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp input::input-placeholder {
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todoapp h1 {
|
||||||
|
position: absolute;
|
||||||
|
top: -155px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 100px;
|
||||||
|
font-weight: 100;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(175, 47, 47, 0.15);
|
||||||
|
-webkit-text-rendering: optimizeLegibility;
|
||||||
|
-moz-text-rendering: optimizeLegibility;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-todo,
|
||||||
|
.edit {
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: 1.4em;
|
||||||
|
border: 0;
|
||||||
|
color: inherit;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid #999;
|
||||||
|
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-todo {
|
||||||
|
padding: 16px 16px 16px 60px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(0, 0, 0, 0.003);
|
||||||
|
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
border-top: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-all {
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
border: none; /* Mobile Safari */
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 100%;
|
||||||
|
bottom: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-all + label {
|
||||||
|
width: 60px;
|
||||||
|
height: 34px;
|
||||||
|
font-size: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: -52px;
|
||||||
|
left: -13px;
|
||||||
|
-webkit-transform: rotate(90deg);
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-all + label:before {
|
||||||
|
content: "❯";
|
||||||
|
font-size: 22px;
|
||||||
|
color: #e6e6e6;
|
||||||
|
padding: 10px 27px 10px 27px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-all:checked + label:before {
|
||||||
|
color: #737373;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li {
|
||||||
|
position: relative;
|
||||||
|
font-size: 24px;
|
||||||
|
border-bottom: 1px solid #ededed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li.editing {
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li.editing .edit {
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 43px);
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 0 0 0 43px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li.editing .view {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .toggle {
|
||||||
|
text-align: center;
|
||||||
|
width: 40px;
|
||||||
|
/* auto, since non-WebKit browsers doesn't support input styling */
|
||||||
|
height: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto 0;
|
||||||
|
border: none; /* Mobile Safari */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .toggle {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .toggle + label {
|
||||||
|
/*
|
||||||
|
Firefox requires \`#\` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
|
||||||
|
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the \`#\` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
|
||||||
|
*/
|
||||||
|
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .toggle:checked + label {
|
||||||
|
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li label {
|
||||||
|
word-break: break-all;
|
||||||
|
padding: 15px 15px 15px 60px;
|
||||||
|
display: block;
|
||||||
|
line-height: 1.2;
|
||||||
|
transition: color 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li.completed label {
|
||||||
|
color: #d9d9d9;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .destroy {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 10px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin: auto 0;
|
||||||
|
font-size: 30px;
|
||||||
|
color: #cc9a9a;
|
||||||
|
margin-bottom: 11px;
|
||||||
|
transition: color 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .destroy:hover {
|
||||||
|
color: #af5b5e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .destroy:after {
|
||||||
|
content: "×";
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li:hover .destroy {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .edit {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li.editing:last-child {
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
color: #777;
|
||||||
|
padding: 10px 15px;
|
||||||
|
height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 50px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
|
||||||
|
0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
|
||||||
|
0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-count {
|
||||||
|
float: left;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-count strong {
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters li {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters li a {
|
||||||
|
color: inherit;
|
||||||
|
margin: 3px;
|
||||||
|
padding: 3px 7px;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters li a:hover {
|
||||||
|
border-color: rgba(175, 47, 47, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters li a.selected {
|
||||||
|
border-color: rgba(175, 47, 47, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-completed,
|
||||||
|
html .clear-completed:active {
|
||||||
|
float: right;
|
||||||
|
position: relative;
|
||||||
|
line-height: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-completed:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
margin: 65px auto 0;
|
||||||
|
color: #bfbfbf;
|
||||||
|
font-size: 10px;
|
||||||
|
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info p {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Hack to remove background from Mobile Safari.
|
||||||
|
Can't use it globally since it destroys checkboxes in Firefox
|
||||||
|
*/
|
||||||
|
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||||
|
.toggle-all,
|
||||||
|
.todo-list li .toggle {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list li .toggle {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 430px) {
|
||||||
|
.footer {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
// This example is an implementation of the TodoList application, from the
|
||||||
|
// www.todomvc.com project. This is a non trivial application with some
|
||||||
|
// interesting user interactions. It uses the local storage for persistence.
|
||||||
|
//
|
||||||
|
// In this implementation, we use the owl reactivity mechanism.
|
||||||
|
import { Component, useState, mount, useRef, reactive, useEnv, useEffect } from "@odoo/owl";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Constants, helpers
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
const ENTER_KEY = 13;
|
||||||
|
const ESC_KEY = 27;
|
||||||
|
|
||||||
|
function useAutofocus(name) {
|
||||||
|
let ref = useRef(name);
|
||||||
|
useEffect(el => el && el.focus(), () => [ref.el]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useStore() {
|
||||||
|
const env = useEnv();
|
||||||
|
return useState(env.store);
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Task store
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
class TaskList {
|
||||||
|
constructor(tasks) {
|
||||||
|
this.tasks = tasks || [];
|
||||||
|
const taskIds = this.tasks.map((t) => t.id);
|
||||||
|
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTask(text) {
|
||||||
|
text = text.trim();
|
||||||
|
if (text) {
|
||||||
|
const task = {
|
||||||
|
id: this.nextId++,
|
||||||
|
text: text,
|
||||||
|
isCompleted: false,
|
||||||
|
};
|
||||||
|
this.tasks.push(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTask(id) {
|
||||||
|
const task = this.tasks.find(t => t.id === id);
|
||||||
|
task.isCompleted = !task.isCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleAll(value) {
|
||||||
|
for (let task of this.tasks) {
|
||||||
|
task.isCompleted = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCompleted() {
|
||||||
|
const tasks = this.tasks.filter(t => t.isCompleted);
|
||||||
|
for (let task of tasks) {
|
||||||
|
this.deleteTask(task.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTask(id) {
|
||||||
|
const index = this.tasks.findIndex((t) => t.id === id);
|
||||||
|
this.tasks.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTask(id, text) {
|
||||||
|
const value = text.trim();
|
||||||
|
if (!value) {
|
||||||
|
this.deleteTask(id);
|
||||||
|
} else {
|
||||||
|
const task = this.tasks.find(t => t.id === id);
|
||||||
|
task.text = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTaskStore() {
|
||||||
|
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
|
||||||
|
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
|
||||||
|
const taskStore = reactive(new TaskList(initialTasks), saveTasks);
|
||||||
|
saveTasks();
|
||||||
|
return taskStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Todo
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
class Todo extends Component {
|
||||||
|
static template = "Todo";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
useAutofocus("input");
|
||||||
|
this.store = useStore();
|
||||||
|
this.state = useState({
|
||||||
|
isEditing: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyup(ev) {
|
||||||
|
if (ev.keyCode === ENTER_KEY) {
|
||||||
|
this.updateText(ev.target.value);
|
||||||
|
}
|
||||||
|
if (ev.keyCode === ESC_KEY) {
|
||||||
|
ev.target.value = this.props.text;
|
||||||
|
this.state.isEditing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBlur(ev) {
|
||||||
|
this.updateText(ev.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateText(text) {
|
||||||
|
this.store.updateTask(this.props.id, text);
|
||||||
|
this.state.isEditing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// TodoList
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
class TodoList extends Component {
|
||||||
|
static template = "TodoList";
|
||||||
|
static components = { Todo };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.store = useStore();
|
||||||
|
this.state = useState({ filter: "all" });
|
||||||
|
}
|
||||||
|
|
||||||
|
get displayedTasks() {
|
||||||
|
const tasks = this.store.tasks;
|
||||||
|
switch (this.state.filter) {
|
||||||
|
case "active":
|
||||||
|
return tasks.filter((t) => !t.isCompleted);
|
||||||
|
case "completed":
|
||||||
|
return tasks.filter((t) => t.isCompleted);
|
||||||
|
case "all":
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get allChecked() {
|
||||||
|
return this.store.tasks.every(todo => todo.isCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
get remaining() {
|
||||||
|
return this.store.tasks.filter(todo => !todo.isCompleted).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get remainingText() {
|
||||||
|
const items = this.remaining < 2 ? "item" : "items";
|
||||||
|
return ` ${items} left`;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTodo(ev) {
|
||||||
|
if (ev.keyCode === ENTER_KEY) {
|
||||||
|
const text = ev.target.value;
|
||||||
|
if (text.trim()) {
|
||||||
|
this.store.addTask(text);
|
||||||
|
}
|
||||||
|
ev.target.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilter(filter) {
|
||||||
|
this.state.filter = filter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// App Initialization
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
const env = { store: createTaskStore() };
|
||||||
|
mount(TodoList, document.body, { env, templates: TEMPLATES, dev: true });
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<templates>
|
||||||
|
<section t-name="TodoList" class="todoapp">
|
||||||
|
<header class="header">
|
||||||
|
<h1>todos</h1>
|
||||||
|
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
|
||||||
|
</header>
|
||||||
|
<section class="main" t-if="store.tasks.length">
|
||||||
|
<input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="() => store.toggleAll(!allChecked)"/>
|
||||||
|
<label for="toggle-all"></label>
|
||||||
|
<ul class="todo-list">
|
||||||
|
<t t-foreach="displayedTasks" t-as="todo" t-key="todo.id">
|
||||||
|
<Todo id="todo.id" isCompleted="todo.isCompleted" text="todo.text"/>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<footer class="footer" t-if="store.tasks.length">
|
||||||
|
<span class="todo-count">
|
||||||
|
<strong>
|
||||||
|
<t t-esc="remaining"/>
|
||||||
|
</strong>
|
||||||
|
<t t-esc="remainingText"/>
|
||||||
|
</span>
|
||||||
|
<ul class="filters">
|
||||||
|
<li>
|
||||||
|
<a t-on-click="() => this.setFilter('all')" t-att-class="{selected: state.filter === 'all'}">All</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a t-on-click="() => this.setFilter('active')" t-att-class="{selected: state.filter === 'active'}">Active</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a t-on-click="() => this.setFilter('completed')" t-att-class="{selected: state.filter === 'completed'}">Completed</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button class="clear-completed" t-if="store.tasks.length gt remaining" t-on-click="() => store.clearCompleted()">
|
||||||
|
Clear completed
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<li t-name="Todo" class="todo" t-att-class="{completed: props.isCompleted, editing: state.isEditing}">
|
||||||
|
<div class="view">
|
||||||
|
<input class="toggle" type="checkbox" t-on-change="() => store.toggleTask(props.id)" t-att-checked="props.completed"/>
|
||||||
|
<label t-on-dblclick="() => state.isEditing = true">
|
||||||
|
<t t-esc="props.text"/>
|
||||||
|
</label>
|
||||||
|
<button class="destroy" t-on-click="() => store.deleteTask(props.id)"></button>
|
||||||
|
</div>
|
||||||
|
<input class="edit" t-ref="input" t-if="state.isEditing" t-att-value="props.text" t-on-keyup="handleKeyup" t-on-blur="handleBlur"/>
|
||||||
|
</li>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-manager {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #eeeeee;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menubar {
|
||||||
|
background-color: #875a7b;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menubar button {
|
||||||
|
height: 40px;
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 30px auto;
|
||||||
|
border: 1px solid gray;
|
||||||
|
background-color: white;
|
||||||
|
position: absolute;
|
||||||
|
box-shadow: 1px 1px 2px 1px grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window.dragging {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window .header {
|
||||||
|
background-color: #875a7b;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 24px;
|
||||||
|
color: white;
|
||||||
|
line-height: 30px;
|
||||||
|
padding-left: 5px;
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window .header .close {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 22px;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.counter button {
|
||||||
|
width: 80px;
|
||||||
|
height:40px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
// This example is slightly more complex than usual. We demonstrate
|
||||||
|
// here a way to manage sub windows in Owl, declaratively. This is still just a
|
||||||
|
// demonstration. Managing windows can be as complex as we want. For example,
|
||||||
|
// we could implement the following features:
|
||||||
|
// - resizing windows
|
||||||
|
// - minimizing windows
|
||||||
|
// - configuration options for windows to make a window non resizeable
|
||||||
|
// - minimal width/height
|
||||||
|
// - better heuristic for initial window position
|
||||||
|
// - ...
|
||||||
|
import { Component, useState, mount, useRef, reactive, useEnv, onMounted } from "@odoo/owl";
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Window manager code
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class WindowManager {
|
||||||
|
// contains all components with metadata
|
||||||
|
static Windows = {};
|
||||||
|
windows = {}; // mapping id => info
|
||||||
|
nextId = 1;
|
||||||
|
|
||||||
|
add(type) {
|
||||||
|
const Comp = WindowManager.Windows[type];
|
||||||
|
const left = 50 + Math.round(Math.random()*(window.innerWidth - 50 - Comp.defaultWidth));
|
||||||
|
const top = 50 + Math.round(Math.random()*(window.innerHeight - 100 - Comp.defaultHeight));
|
||||||
|
const id = this.nextId++;
|
||||||
|
this.windows[id] = {
|
||||||
|
id,
|
||||||
|
title: Comp.defaultTitle,
|
||||||
|
width: Comp.defaultWidth,
|
||||||
|
height: Comp.defaultHeight,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
Component: Comp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
close(id) {
|
||||||
|
delete this.windows[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePosition(id, left, top) {
|
||||||
|
const w = this.windows[id];
|
||||||
|
w.left = left;
|
||||||
|
w.top = top;
|
||||||
|
}
|
||||||
|
|
||||||
|
getWindows() {
|
||||||
|
return Object.values(this.windows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWindowService() {
|
||||||
|
return reactive(new WindowManager());
|
||||||
|
}
|
||||||
|
|
||||||
|
function useWindowService() {
|
||||||
|
const env = useEnv();
|
||||||
|
return useState(env.windowService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Generic Window Component
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class Window extends Component {
|
||||||
|
static template = "Window";
|
||||||
|
static nextZIndex = 1;
|
||||||
|
zIndex = 0;
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.windowService = useWindowService();
|
||||||
|
this.root = useRef('root');
|
||||||
|
onMounted(this.updateZIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
get style() {
|
||||||
|
let { width, height, top, left } = this.props.info;
|
||||||
|
return `width: ${width}px;height: ${height}px;top:${top}px;left:${left}px;z-index:${this.zIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.windowService.close(this.props.info.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
startDragAndDrop(ev) {
|
||||||
|
this.updateZIndex();
|
||||||
|
const self = this;
|
||||||
|
const root = this.root;
|
||||||
|
|
||||||
|
const el = root.el;
|
||||||
|
el.classList.add('dragging');
|
||||||
|
|
||||||
|
const current = this.props.info;
|
||||||
|
const offsetX = current.left - ev.pageX;
|
||||||
|
const offsetY = current.top - ev.pageY;
|
||||||
|
let left, top;
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", moveWindow);
|
||||||
|
window.addEventListener("mouseup", stopDnD, { once: true });
|
||||||
|
|
||||||
|
function moveWindow(ev) {
|
||||||
|
left = Math.max(offsetX + ev.pageX, 0);
|
||||||
|
top = Math.max(offsetY + ev.pageY, 0);
|
||||||
|
el.style.left = `${left}px`;
|
||||||
|
el.style.top = `${top}px`;
|
||||||
|
}
|
||||||
|
function stopDnD() {
|
||||||
|
window.removeEventListener("mousemove", moveWindow);
|
||||||
|
el.classList.remove('dragging');
|
||||||
|
|
||||||
|
if (top !== undefined && left !== undefined) {
|
||||||
|
self.windowService.updatePosition(current.id, left, top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateZIndex() {
|
||||||
|
this.zIndex = Window.nextZIndex++;
|
||||||
|
this.root.el.style['z-index'] = this.zIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Two concrete Window type implementations
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class HelloWorld extends Component {
|
||||||
|
static template = "HelloWorld";
|
||||||
|
static defaultTitle = "Hello Owl!";
|
||||||
|
static defaultWidth = 200;
|
||||||
|
static defaultHeight = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Counter extends Component {
|
||||||
|
static template = "Counter";
|
||||||
|
static defaultTitle = "Click Counter";
|
||||||
|
static defaultWidth = 300;
|
||||||
|
static defaultHeight = 120;
|
||||||
|
|
||||||
|
state = useState({ value: 0 });
|
||||||
|
|
||||||
|
inc() {
|
||||||
|
this.state.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// register window components
|
||||||
|
WindowManager.Windows.Hello = HelloWorld;
|
||||||
|
WindowManager.Windows.Counter = Counter;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Window Container
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class WindowContainer extends Component {
|
||||||
|
static template = "WindowContainer";
|
||||||
|
static components = { Window };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.windowService = useWindowService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Root Component
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class Root extends Component {
|
||||||
|
static template = "Root";
|
||||||
|
static components = { WindowContainer };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.windowService = useWindowService();
|
||||||
|
}
|
||||||
|
|
||||||
|
addWindow(type) {
|
||||||
|
this.windowService.add(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Setup
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const env = {
|
||||||
|
windowService: createWindowService(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<templates>
|
||||||
|
<div t-name="Window" class="window" t-att-style="style" t-on-click="updateZIndex" t-ref="root">
|
||||||
|
<div class="header">
|
||||||
|
<span t-on-mousedown="startDragAndDrop"><t t-esc="props.info.title"/></span>
|
||||||
|
<span class="close" t-on-click.stop="close">×</span>
|
||||||
|
</div>
|
||||||
|
<t t-slot="default"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="WindowContainer" class="window-manager">
|
||||||
|
<Window t-foreach="windowService.getWindows()" t-as="w" t-key="w.id" info="w">
|
||||||
|
<t t-component="w.Component"/>
|
||||||
|
</Window>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="Root" class="app">
|
||||||
|
<WindowContainer/>
|
||||||
|
<div class="menubar">
|
||||||
|
<button t-on-click="() => this.addWindow('Hello')">Say Hello</button>
|
||||||
|
<button t-on-click="() => this.addWindow('Counter')">Counter</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="HelloWorld">
|
||||||
|
Some content here...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="Counter" class="counter">
|
||||||
|
<button t-on-click="inc">Click</button>
|
||||||
|
<span><t t-esc="state.value"/></span>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
||||||
|
|
||||||
|
httpd = None
|
||||||
|
def start_server():
|
||||||
|
global httpd
|
||||||
|
SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript'
|
||||||
|
httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler)
|
||||||
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
url = 'http://127.0.0.1:3600'
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("Owl Application")
|
||||||
|
print("---------------")
|
||||||
|
print("Server running on: {}".format(url))
|
||||||
|
threading.Thread(target=start_server, daemon=True).start()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
httpd.server_close()
|
||||||
|
quit(0)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>OWL App</title>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="app.css">
|
||||||
|
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
|
||||||
|
<script type="module" src="app.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<pre>
|
||||||
|
This app requires a web server, simply opening the html file using your browser will not work.
|
||||||
|
You can run a simple web server with one of the following:
|
||||||
|
- if you have python3 installed, run app.py with python3 or run 'python3 -m http.server' in the app folder
|
||||||
|
- if you have node/npm installed, run 'npx serve' in the app folder
|
||||||
|
- if you have php installed, run 'php -S localhost:8080 -t .' in the app folder
|
||||||
|
</pre>
|
||||||
|
<script>if (window.location.protocol !== "file:") document.querySelector("pre").remove()</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<templates>
|
<templates>
|
||||||
<div t-name="TabbedEditor" class="tabbed-editor">
|
<div t-name="TabbedEditor" class="tabbed-editor" t-att-style="props.style">
|
||||||
<div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown">
|
<div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown">
|
||||||
<t t-foreach="['js', 'xml', 'css']" t-as="tab" t-key="tab">
|
<t t-foreach="['js', 'xml', 'css']" t-as="tab" t-key="tab">
|
||||||
<a t-ref="{{tab}}" t-if="props[tab] !== false" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="() => setTab(tab)">
|
<a t-ref="{{tab}}" t-if="props[tab] !== false" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="() => setTab(tab)">
|
||||||
@@ -10,17 +10,17 @@
|
|||||||
<div class="code-editor" t-ref="editor"></div>
|
<div class="code-editor" t-ref="editor"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div t-name="App" class="playground">
|
<div t-name="Playground" class="playground">
|
||||||
<div class="left-bar" t-att-class="{split: state.splitLayout}"
|
<div class="left-bar" t-att-class="{split: state.splitLayout}"
|
||||||
t-att-style="leftPaneStyle">
|
t-att-style="leftPaneStyle">
|
||||||
<div class="menubar">
|
<div class="menubar">
|
||||||
<a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a>
|
<a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a>
|
||||||
<select t-on-change="setSample">
|
<select t-on-change="onSampleChange">
|
||||||
<option t-foreach="SAMPLES" t-as="sample" t-key="sample_index">
|
<option t-foreach="samples" t-as="sample" t-key="sample_index">
|
||||||
<t t-esc="sample.description"/>
|
<t t-esc="sample.description"/>
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a>
|
<a class="btn flash" t-on-click="exportStandaloneApp" title="Download zip that bundles this app with a tiny python web server"><i class="fas fa-download"></i></a>
|
||||||
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
|
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
|
||||||
<a class="btn flash share-code" t-on-click="shareCode" title="Copy a link to this playground"><span>Share</span><span class="copied-notification" t-if="state.copied">Copied link to clipboard!</span></a>
|
<a class="btn flash share-code" t-on-click="shareCode" title="Copy a link to this playground"><span>Share</span><span class="copied-notification" t-if="state.copied">Copied link to clipboard!</span></a>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,19 +37,19 @@
|
|||||||
css="state.css"
|
css="state.css"
|
||||||
xml="state.xml"
|
xml="state.xml"
|
||||||
resizeable="true"
|
resizeable="true"
|
||||||
onUpdatePanelHeight="updatePanelHeight"
|
updatePanelHeight.bind="updatePanelHeight"
|
||||||
updateCode="updateCode"/>
|
updateCode="updateCode"/>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
<div class="separator vertical" t-on-mousedown="onMouseDown"/>
|
<div class="separator vertical" t-on-mousedown="onMouseDown"/>
|
||||||
<div class="right-pane" t-att-style="rightPaneStyle">
|
<div class="right-pane">
|
||||||
<div class="welcome" t-if="state.displayWelcome">
|
<div class="welcome" t-if="state.displayWelcome">
|
||||||
<div>🦉 Odoo Web Library 🦉</div>
|
<div>🦉 Odoo Web Library 🦉</div>
|
||||||
<div>v<t t-esc="version"/></div>
|
<div>v<t t-esc="version"/></div>
|
||||||
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div>
|
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div>
|
||||||
<div class="note">
|
<div class="note">
|
||||||
<p>Note: these examples are using recent features of Javascript, and require a recent browser to work without a transpilation step!
|
<p>
|
||||||
For example, it makes use of class fields and class static fields. These examples should work in a recent Chrome version.
|
Note: these examples make use of recent features of Javascript, and require a recent browser to work without a transpilation step! Among other things, it uses class fields and import maps. If you encounter issues, make sure your browser is up to date.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,12 +18,6 @@ export function debounce(func, wait, immediate) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBus(bus, evName, cb) {
|
|
||||||
bus.addEventListener(evName, cb);
|
|
||||||
owl.onWillUnmount(() => {
|
|
||||||
bus.removeEventListener(evName, cb);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadedScripts = {};
|
const loadedScripts = {};
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Github page and playground
|
||||||
|
|
||||||
|
This folder contains the code for owl's [github page](https://odoo.github.io/owl)
|
||||||
|
and the owl [playground](https://odoo.github.io/owl/playground/). If you're
|
||||||
|
looking for the owl documentation, click [here](/doc)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@odoo/owl",
|
"name": "@odoo/owl",
|
||||||
"version": "2.1.1",
|
"version": "2.8.1",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"description": "Odoo Web Library (OWL)",
|
||||||
"main": "dist/owl.cjs.js",
|
"main": "dist/owl.cjs.js",
|
||||||
"module": "dist/owl.es.js",
|
"module": "dist/owl.es.js",
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.18.3"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:bundle": "rollup -c --failAfterWarnings",
|
"build:bundle": "rollup -c --failAfterWarnings",
|
||||||
@@ -24,16 +24,18 @@
|
|||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
|
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"playground:serve": "python3 tools/server.py || python tools/server.py",
|
"playground:serve": "python3 tools/playground_server.py || python tools/playground_server.py",
|
||||||
"playground": "npm run build && npm run playground:serve",
|
"playground": "npm run build && npm run playground:serve",
|
||||||
"preplayground:watch": "npm run build",
|
"preplayground:watch": "npm run build",
|
||||||
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
|
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
|
||||||
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --write",
|
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --write",
|
||||||
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --check",
|
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --check",
|
||||||
"lint": "eslint src/**/*.ts tests/**/*.ts",
|
"lint": "eslint src/**/*.ts tests/**/*.ts",
|
||||||
"publish": "npm run build && npm publish",
|
|
||||||
"release": "node tools/release.js",
|
"release": "node tools/release.js",
|
||||||
"compile_templates": "node tools/compile_xml.js"
|
"compile_templates": "node tools/compile_owl_templates.mjs"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"compile_owl_templates": "tools/compile_owl_templates.mjs"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -47,6 +49,7 @@
|
|||||||
"homepage": "https://github.com/odoo/owl#readme",
|
"homepage": "https://github.com/odoo/owl#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^27.0.1",
|
"@types/jest": "^27.0.1",
|
||||||
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/node": "^14.11.8",
|
"@types/node": "^14.11.8",
|
||||||
"@typescript-eslint/eslint-plugin": "5.48.1",
|
"@typescript-eslint/eslint-plugin": "5.48.1",
|
||||||
"@typescript-eslint/parser": "5.48.1",
|
"@typescript-eslint/parser": "5.48.1",
|
||||||
@@ -98,5 +101,8 @@
|
|||||||
"prettier": {
|
"prettier": {
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"endOfLine": "auto"
|
"endOfLine": "auto"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"jsdom": "^25.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import pkg from "./package.json";
|
import pkg from "./package.json";
|
||||||
import git from "git-rev-sync";
|
import git from "git-rev-sync";
|
||||||
import typescript from 'rollup-plugin-typescript2';
|
import typescript from "rollup-plugin-typescript2";
|
||||||
import { terser } from "rollup-plugin-terser";
|
import { terser } from "rollup-plugin-terser";
|
||||||
import dts from "rollup-plugin-dts";
|
import dts from "rollup-plugin-dts";
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ const ES_FILENAME = "dist/owl.es.js";
|
|||||||
|
|
||||||
if (pkg.module !== ES_FILENAME || pkg.main !== CJS_FILENAME) {
|
if (pkg.module !== ES_FILENAME || pkg.main !== CJS_FILENAME) {
|
||||||
throw new Error("package.json has been modified. Build script should be updated accordingly");
|
throw new Error("package.json has been modified. Build script should be updated accordingly");
|
||||||
}
|
}
|
||||||
|
|
||||||
const outro = `
|
const outro = `
|
||||||
__info__.date = '${new Date().toISOString()}';
|
__info__.date = '${new Date().toISOString()}';
|
||||||
@@ -21,39 +21,37 @@ __info__.url = 'https://github.com/odoo/owl';
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
switch (process.argv[4]) {
|
switch (process.argv[4]) {
|
||||||
case "compiler":
|
case "compiler":
|
||||||
input = "src/compiler/index.ts",
|
(input = "src/compiler/index.ts"),
|
||||||
output = [
|
(output = [getConfigForFormat("cjs", "dist/compiler.js", "")]);
|
||||||
getConfigForFormat('cjs', 'dist/compiler.js', ''),
|
|
||||||
]
|
|
||||||
break;
|
break;
|
||||||
case "runtime":
|
case "runtime":
|
||||||
input = "src/runtime/index.ts";
|
input = "src/runtime/index.ts";
|
||||||
output = [
|
output = [
|
||||||
getConfigForFormat('esm', addSuffix(ES_FILENAME, 'runtime'), outro),
|
getConfigForFormat("esm", addSuffix(ES_FILENAME, "runtime"), outro),
|
||||||
getConfigForFormat('cjs', addSuffix(CJS_FILENAME, 'runtime'), outro),
|
getConfigForFormat("cjs", addSuffix(CJS_FILENAME, "runtime"), outro),
|
||||||
getConfigForFormat('iife', addSuffix(IIFE_FILENAME, 'runtime'), outro),
|
getConfigForFormat("iife", addSuffix(IIFE_FILENAME, "runtime"), outro),
|
||||||
getConfigForFormat('iife', addSuffix(IIFE_FILENAME, 'runtime'), outro, true),
|
getConfigForFormat("iife", addSuffix(IIFE_FILENAME, "runtime"), outro, true),
|
||||||
]
|
];
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
input = "src/index.ts",
|
(input = "src/index.ts"),
|
||||||
output = [
|
(output = [
|
||||||
getConfigForFormat('esm', ES_FILENAME, outro),
|
getConfigForFormat("esm", ES_FILENAME, outro),
|
||||||
getConfigForFormat('cjs', CJS_FILENAME, outro),
|
getConfigForFormat("cjs", CJS_FILENAME, outro),
|
||||||
getConfigForFormat('iife', IIFE_FILENAME, outro),
|
getConfigForFormat("iife", IIFE_FILENAME, outro),
|
||||||
getConfigForFormat('iife', IIFE_FILENAME, outro, true),
|
getConfigForFormat("iife", IIFE_FILENAME, outro, true),
|
||||||
]
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate from a string depicting a path a new path for the minified version.
|
* Generate from a string depicting a path a new path for the minified version.
|
||||||
* @param {string} pkgFileName file name
|
* @param {string} pkgFileName file name
|
||||||
*/
|
*/
|
||||||
function addSuffix(pkgFileName, suffix) {
|
function addSuffix(pkgFileName, suffix) {
|
||||||
const parts = pkgFileName.split('.');
|
const parts = pkgFileName.split(".");
|
||||||
parts.splice(parts.length - 1, 0, suffix);
|
parts.splice(parts.length - 1, 0, suffix);
|
||||||
return parts.join('.');
|
return parts.join(".");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,7 +69,7 @@ function getConfigForFormat(format, generatedFileName, outro, minified = false)
|
|||||||
outro: outro,
|
outro: outro,
|
||||||
freeze: false,
|
freeze: false,
|
||||||
plugins: minified ? [terser()] : [],
|
plugins: minified ? [terser()] : [],
|
||||||
indent: ' ', // indent with 4 spaces
|
indent: " ", // indent with 4 spaces
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,9 +79,19 @@ export default [
|
|||||||
output,
|
output,
|
||||||
plugins: [
|
plugins: [
|
||||||
typescript({
|
typescript({
|
||||||
useTsconfigDeclarationDir: true
|
useTsconfigDeclarationDir: true,
|
||||||
}),
|
}),
|
||||||
]
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "src/compiler/standalone/index.ts",
|
||||||
|
output: [{ file: "dist/compile_templates.mjs", format: "es" }],
|
||||||
|
external: ["fs", "fs/promises", "path", "jsdom"],
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
useTsconfigDeclarationDir: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "dist/types/index.d.ts",
|
input: "dist/types/index.d.ts",
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// Custom error class that wraps error that happen in the owl lifecycle
|
||||||
|
export class OwlError extends Error {
|
||||||
|
cause?: any;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export type customDirectives = Record<
|
||||||
|
string,
|
||||||
|
(node: Element, value: string, modifier: string[]) => void
|
||||||
|
>;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { OwlError } from "./owl_error";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an XML string into an XML document, throwing errors on parser errors
|
||||||
|
* instead of returning an XML document containing the parseerror.
|
||||||
|
*
|
||||||
|
* @param xml the string to parse
|
||||||
|
* @returns an XML document corresponding to the content of the string
|
||||||
|
*/
|
||||||
|
export function parseXML(xml: string): XMLDocument {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(xml, "text/xml");
|
||||||
|
if (doc.getElementsByTagName("parsererror").length) {
|
||||||
|
let msg = "Invalid XML in template.";
|
||||||
|
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
|
||||||
|
if (parsererrorText) {
|
||||||
|
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
|
||||||
|
const re = /\d+/g;
|
||||||
|
const firstMatch = re.exec(parsererrorText);
|
||||||
|
if (firstMatch) {
|
||||||
|
const lineNumber = Number(firstMatch[0]);
|
||||||
|
const line = xml.split("\n")[lineNumber - 1];
|
||||||
|
const secondMatch = re.exec(parsererrorText);
|
||||||
|
if (line && secondMatch) {
|
||||||
|
const columnIndex = Number(secondMatch[0]) - 1;
|
||||||
|
if (line[columnIndex]) {
|
||||||
|
msg +=
|
||||||
|
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
|
||||||
|
`${line}\n${"-".repeat(columnIndex - 1)}^`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new OwlError(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
@@ -24,18 +24,19 @@ import {
|
|||||||
ASTTOut,
|
ASTTOut,
|
||||||
ASTTPortal,
|
ASTTPortal,
|
||||||
ASTTranslation,
|
ASTTranslation,
|
||||||
|
ASTTranslationContext,
|
||||||
ASTTSet,
|
ASTTSet,
|
||||||
ASTType,
|
ASTType,
|
||||||
Attrs,
|
Attrs,
|
||||||
EventHandlers,
|
EventHandlers,
|
||||||
} from "./parser";
|
} from "./parser";
|
||||||
import { OwlError } from "../runtime/error_handling";
|
import { OwlError } from "../common/owl_error";
|
||||||
|
|
||||||
type BlockType = "block" | "text" | "multi" | "list" | "html" | "comment";
|
type BlockType = "block" | "text" | "multi" | "list" | "html" | "comment";
|
||||||
const whitespaceRE = /\s+/g;
|
const whitespaceRE = /\s+/g;
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
translateFn?: (s: string) => string;
|
translateFn?: (s: string, translationCtx: string) => string;
|
||||||
translatableAttributes?: string[];
|
translatableAttributes?: string[];
|
||||||
dev?: boolean;
|
dev?: boolean;
|
||||||
}
|
}
|
||||||
@@ -43,6 +44,7 @@ export interface Config {
|
|||||||
export interface CodeGenOptions extends Config {
|
export interface CodeGenOptions extends Config {
|
||||||
hasSafeContext?: boolean;
|
hasSafeContext?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
hasGlobalValues: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// using a non-html document so that <inner/outer>HTML serializes as XML instead
|
// using a non-html document so that <inner/outer>HTML serializes as XML instead
|
||||||
@@ -82,6 +84,14 @@ function isProp(tag: string, key: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a template literal that evaluates to str. You can add interpolation
|
||||||
|
* sigils into the string if required
|
||||||
|
*/
|
||||||
|
function toStringExpression(str: string) {
|
||||||
|
return `\`${str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/, "\\${")}\``;
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// BlockDescription
|
// BlockDescription
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -160,9 +170,9 @@ interface Context {
|
|||||||
block: BlockDescription | null;
|
block: BlockDescription | null;
|
||||||
index: number | string;
|
index: number | string;
|
||||||
forceNewBlock: boolean;
|
forceNewBlock: boolean;
|
||||||
preventRoot?: boolean;
|
|
||||||
isLast?: boolean;
|
isLast?: boolean;
|
||||||
translate: boolean;
|
translate: boolean;
|
||||||
|
translationCtx: string;
|
||||||
tKeyExpr: string | null;
|
tKeyExpr: string | null;
|
||||||
nameSpace?: string;
|
nameSpace?: string;
|
||||||
tModelSelectedExpr?: string;
|
tModelSelectedExpr?: string;
|
||||||
@@ -177,6 +187,7 @@ function createContext(parentCtx: Context, params?: Partial<Context>): Context {
|
|||||||
index: 0,
|
index: 0,
|
||||||
forceNewBlock: true,
|
forceNewBlock: true,
|
||||||
translate: parentCtx.translate,
|
translate: parentCtx.translate,
|
||||||
|
translationCtx: parentCtx.translationCtx,
|
||||||
tKeyExpr: null,
|
tKeyExpr: null,
|
||||||
nameSpace: parentCtx.nameSpace,
|
nameSpace: parentCtx.nameSpace,
|
||||||
tModelSelectedExpr: parentCtx.tModelSelectedExpr,
|
tModelSelectedExpr: parentCtx.tModelSelectedExpr,
|
||||||
@@ -243,7 +254,16 @@ class CodeTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
const TRANSLATABLE_ATTRS = [
|
||||||
|
"alt",
|
||||||
|
"aria-label",
|
||||||
|
"aria-placeholder",
|
||||||
|
"aria-roledescription",
|
||||||
|
"aria-valuetext",
|
||||||
|
"label",
|
||||||
|
"placeholder",
|
||||||
|
"title",
|
||||||
|
];
|
||||||
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
||||||
|
|
||||||
export class CodeGenerator {
|
export class CodeGenerator {
|
||||||
@@ -255,7 +275,7 @@ export class CodeGenerator {
|
|||||||
target = new CodeTarget("template");
|
target = new CodeTarget("template");
|
||||||
templateName?: string;
|
templateName?: string;
|
||||||
dev: boolean;
|
dev: boolean;
|
||||||
translateFn: (s: string) => string;
|
translateFn: (s: string, translationCtx: string) => string;
|
||||||
translatableAttributes: string[] = TRANSLATABLE_ATTRS;
|
translatableAttributes: string[] = TRANSLATABLE_ATTRS;
|
||||||
ast: AST;
|
ast: AST;
|
||||||
staticDefs: { id: string; expr: string }[] = [];
|
staticDefs: { id: string; expr: string }[] = [];
|
||||||
@@ -279,6 +299,9 @@ export class CodeGenerator {
|
|||||||
this.dev = options.dev || false;
|
this.dev = options.dev || false;
|
||||||
this.ast = ast;
|
this.ast = ast;
|
||||||
this.templateName = options.name;
|
this.templateName = options.name;
|
||||||
|
if (options.hasGlobalValues) {
|
||||||
|
this.helpers.add("__globals__");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
generateCode(): string {
|
generateCode(): string {
|
||||||
@@ -292,6 +315,7 @@ export class CodeGenerator {
|
|||||||
forceNewBlock: false,
|
forceNewBlock: false,
|
||||||
isLast: true,
|
isLast: true,
|
||||||
translate: true,
|
translate: true,
|
||||||
|
translationCtx: "",
|
||||||
tKeyExpr: null,
|
tKeyExpr: null,
|
||||||
});
|
});
|
||||||
// define blocks and utility functions
|
// define blocks and utility functions
|
||||||
@@ -312,14 +336,13 @@ export class CodeGenerator {
|
|||||||
mainCode.push(``);
|
mainCode.push(``);
|
||||||
for (let block of this.blocks) {
|
for (let block of this.blocks) {
|
||||||
if (block.dom) {
|
if (block.dom) {
|
||||||
let xmlString = block.asXmlString();
|
let xmlString = toStringExpression(block.asXmlString());
|
||||||
xmlString = xmlString.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
||||||
if (block.dynamicTagName) {
|
if (block.dynamicTagName) {
|
||||||
xmlString = xmlString.replace(/^<\w+/, `<\${tag || '${block.dom.nodeName}'}`);
|
xmlString = xmlString.replace(/^`<\w+/, `\`<\${tag || '${block.dom.nodeName}'}`);
|
||||||
xmlString = xmlString.replace(/\w+>$/, `\${tag || '${block.dom.nodeName}'}>`);
|
xmlString = xmlString.replace(/\w+>`$/, `\${tag || '${block.dom.nodeName}'}>\``);
|
||||||
mainCode.push(`let ${block.blockName} = tag => createBlock(\`${xmlString}\`);`);
|
mainCode.push(`let ${block.blockName} = tag => createBlock(${xmlString});`);
|
||||||
} else {
|
} else {
|
||||||
mainCode.push(`let ${block.blockName} = createBlock(\`${xmlString}\`);`);
|
mainCode.push(`let ${block.blockName} = createBlock(${xmlString});`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,7 +400,7 @@ export class CodeGenerator {
|
|||||||
): BlockDescription {
|
): BlockDescription {
|
||||||
const hasRoot = this.target.hasRoot;
|
const hasRoot = this.target.hasRoot;
|
||||||
const block = new BlockDescription(this.target, type);
|
const block = new BlockDescription(this.target, type);
|
||||||
if (!hasRoot && !ctx.preventRoot) {
|
if (!hasRoot) {
|
||||||
this.target.hasRoot = true;
|
this.target.hasRoot = true;
|
||||||
block.isRoot = true;
|
block.isRoot = true;
|
||||||
}
|
}
|
||||||
@@ -403,7 +426,7 @@ export class CodeGenerator {
|
|||||||
blockExpr = `toggler(${ctx.tKeyExpr}, ${blockExpr})`;
|
blockExpr = `toggler(${ctx.tKeyExpr}, ${blockExpr})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (block.isRoot && !ctx.preventRoot) {
|
if (block.isRoot) {
|
||||||
if (this.target.on) {
|
if (this.target.on) {
|
||||||
blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on);
|
blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on);
|
||||||
}
|
}
|
||||||
@@ -447,6 +470,11 @@ export class CodeGenerator {
|
|||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
translate(str: string, translationCtx: string): string {
|
||||||
|
const match = translationRE.exec(str) as any;
|
||||||
|
return match[1] + this.translateFn(match[2], translationCtx) + match[3];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns the newly created block name, if any
|
* @returns the newly created block name, if any
|
||||||
*/
|
*/
|
||||||
@@ -486,6 +514,8 @@ export class CodeGenerator {
|
|||||||
return this.compileTSlot(ast, ctx);
|
return this.compileTSlot(ast, ctx);
|
||||||
case ASTType.TTranslation:
|
case ASTType.TTranslation:
|
||||||
return this.compileTTranslation(ast, ctx);
|
return this.compileTTranslation(ast, ctx);
|
||||||
|
case ASTType.TTranslationContext:
|
||||||
|
return this.compileTTranslationContext(ast, ctx);
|
||||||
case ASTType.TPortal:
|
case ASTType.TPortal:
|
||||||
return this.compileTPortal(ast, ctx);
|
return this.compileTPortal(ast, ctx);
|
||||||
}
|
}
|
||||||
@@ -511,7 +541,7 @@ export class CodeGenerator {
|
|||||||
const isNewBlock = !block || forceNewBlock;
|
const isNewBlock = !block || forceNewBlock;
|
||||||
if (isNewBlock) {
|
if (isNewBlock) {
|
||||||
block = this.createBlock(block, "comment", ctx);
|
block = this.createBlock(block, "comment", ctx);
|
||||||
this.insertBlock(`comment(\`${ast.value}\`)`, block, {
|
this.insertBlock(`comment(${toStringExpression(ast.value)})`, block, {
|
||||||
...ctx,
|
...ctx,
|
||||||
forceNewBlock: forceNewBlock && !block,
|
forceNewBlock: forceNewBlock && !block,
|
||||||
});
|
});
|
||||||
@@ -527,8 +557,7 @@ export class CodeGenerator {
|
|||||||
|
|
||||||
let value = ast.value;
|
let value = ast.value;
|
||||||
if (value && ctx.translate !== false) {
|
if (value && ctx.translate !== false) {
|
||||||
const match = translationRE.exec(value) as any;
|
value = this.translate(value, ctx.translationCtx);
|
||||||
value = match[1] + this.translateFn(match[2]) + match[3];
|
|
||||||
}
|
}
|
||||||
if (!ctx.inPreTag) {
|
if (!ctx.inPreTag) {
|
||||||
value = value.replace(whitespaceRE, " ");
|
value = value.replace(whitespaceRE, " ");
|
||||||
@@ -536,7 +565,7 @@ export class CodeGenerator {
|
|||||||
|
|
||||||
if (!block || forceNewBlock) {
|
if (!block || forceNewBlock) {
|
||||||
block = this.createBlock(block, "text", ctx);
|
block = this.createBlock(block, "text", ctx);
|
||||||
this.insertBlock(`text(\`${value}\`)`, block, {
|
this.insertBlock(`text(${toStringExpression(value)})`, block, {
|
||||||
...ctx,
|
...ctx,
|
||||||
forceNewBlock: forceNewBlock && !block,
|
forceNewBlock: forceNewBlock && !block,
|
||||||
});
|
});
|
||||||
@@ -582,11 +611,6 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
// attributes
|
// attributes
|
||||||
const attrs: Attrs = {};
|
const attrs: Attrs = {};
|
||||||
const nameSpace = ast.ns || ctx.nameSpace;
|
|
||||||
if (nameSpace && isNewBlock) {
|
|
||||||
// specific namespace uri
|
|
||||||
attrs["block-ns"] = nameSpace;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let key in ast.attrs) {
|
for (let key in ast.attrs) {
|
||||||
let expr, attrName;
|
let expr, attrName;
|
||||||
@@ -622,7 +646,8 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (this.translatableAttributes.includes(key)) {
|
} else if (this.translatableAttributes.includes(key)) {
|
||||||
attrs[key] = this.translateFn(ast.attrs[key]);
|
const attrTranslationCtx = ast.attrsTranslationCtx?.[key] || ctx.translationCtx;
|
||||||
|
attrs[key] = this.translateFn(ast.attrs[key], attrTranslationCtx);
|
||||||
} else {
|
} else {
|
||||||
expr = `"${ast.attrs[key]}"`;
|
expr = `"${ast.attrs[key]}"`;
|
||||||
attrName = key;
|
attrName = key;
|
||||||
@@ -715,7 +740,10 @@ export class CodeGenerator {
|
|||||||
attrs["block-ref"] = String(idx);
|
attrs["block-ref"] = String(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dom = xmlDoc.createElement(ast.tag);
|
const nameSpace = ast.ns || ctx.nameSpace;
|
||||||
|
const dom = nameSpace
|
||||||
|
? xmlDoc.createElementNS(nameSpace, ast.tag)
|
||||||
|
: xmlDoc.createElement(ast.tag);
|
||||||
for (const [attr, val] of Object.entries(attrs)) {
|
for (const [attr, val] of Object.entries(attrs)) {
|
||||||
if (!(attr === "class" && val === "")) {
|
if (!(attr === "class" && val === "")) {
|
||||||
dom.setAttribute(attr, val);
|
dom.setAttribute(attr, val);
|
||||||
@@ -757,7 +785,7 @@ export class CodeGenerator {
|
|||||||
if (!current) break;
|
if (!current) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.addLine(`let ${block!.children.map((c) => c.varName)};`, codeIdx);
|
this.addLine(`let ${block!.children.map((c) => c.varName).join(", ")};`, codeIdx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return block!.varName;
|
return block!.varName;
|
||||||
@@ -773,7 +801,8 @@ export class CodeGenerator {
|
|||||||
expr = compileExpr(ast.expr);
|
expr = compileExpr(ast.expr);
|
||||||
if (ast.defaultValue) {
|
if (ast.defaultValue) {
|
||||||
this.helpers.add("withDefault");
|
this.helpers.add("withDefault");
|
||||||
expr = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
|
// FIXME: defaultValue is not translated
|
||||||
|
expr = `withDefault(${expr}, ${toStringExpression(ast.defaultValue)})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!block || forceNewBlock) {
|
if (!block || forceNewBlock) {
|
||||||
@@ -859,7 +888,7 @@ export class CodeGenerator {
|
|||||||
if (!current) break;
|
if (!current) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.addLine(`let ${block!.children.map((c) => c.varName)};`, codeIdx);
|
this.addLine(`let ${block!.children.map((c) => c.varName).join(", ")};`, codeIdx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// note: this part is duplicated from end of compilemulti:
|
// note: this part is duplicated from end of compilemulti:
|
||||||
@@ -890,18 +919,18 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
this.addLine(`for (let ${loopVar} = 0; ${loopVar} < ${l}; ${loopVar}++) {`);
|
this.addLine(`for (let ${loopVar} = 0; ${loopVar} < ${l}; ${loopVar}++) {`);
|
||||||
this.target.indentLevel++;
|
this.target.indentLevel++;
|
||||||
this.addLine(`ctx[\`${ast.elem}\`] = ${vals}[${loopVar}];`);
|
this.addLine(`ctx[\`${ast.elem}\`] = ${keys}[${loopVar}];`);
|
||||||
if (!ast.hasNoFirst) {
|
if (!ast.hasNoFirst) {
|
||||||
this.addLine(`ctx[\`${ast.elem}_first\`] = ${loopVar} === 0;`);
|
this.addLine(`ctx[\`${ast.elem}_first\`] = ${loopVar} === 0;`);
|
||||||
}
|
}
|
||||||
if (!ast.hasNoLast) {
|
if (!ast.hasNoLast) {
|
||||||
this.addLine(`ctx[\`${ast.elem}_last\`] = ${loopVar} === ${vals}.length - 1;`);
|
this.addLine(`ctx[\`${ast.elem}_last\`] = ${loopVar} === ${keys}.length - 1;`);
|
||||||
}
|
}
|
||||||
if (!ast.hasNoIndex) {
|
if (!ast.hasNoIndex) {
|
||||||
this.addLine(`ctx[\`${ast.elem}_index\`] = ${loopVar};`);
|
this.addLine(`ctx[\`${ast.elem}_index\`] = ${loopVar};`);
|
||||||
}
|
}
|
||||||
if (!ast.hasNoValue) {
|
if (!ast.hasNoValue) {
|
||||||
this.addLine(`ctx[\`${ast.elem}_value\`] = ${keys}[${loopVar}];`);
|
this.addLine(`ctx[\`${ast.elem}_value\`] = ${vals}[${loopVar}];`);
|
||||||
}
|
}
|
||||||
this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar);
|
this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar);
|
||||||
if (this.dev) {
|
if (this.dev) {
|
||||||
@@ -966,7 +995,7 @@ export class CodeGenerator {
|
|||||||
const isNewBlock = !block || forceNewBlock;
|
const isNewBlock = !block || forceNewBlock;
|
||||||
let codeIdx = this.target.code.length;
|
let codeIdx = this.target.code.length;
|
||||||
if (isNewBlock) {
|
if (isNewBlock) {
|
||||||
const n = ast.content.filter((c) => c.type !== ASTType.TSet).length;
|
const n = ast.content.filter((c) => !c.hasNoRepresentation).length;
|
||||||
let result: string | null = null;
|
let result: string | null = null;
|
||||||
if (n <= 1) {
|
if (n <= 1) {
|
||||||
for (let child of ast.content) {
|
for (let child of ast.content) {
|
||||||
@@ -980,34 +1009,31 @@ export class CodeGenerator {
|
|||||||
let index = 0;
|
let index = 0;
|
||||||
for (let i = 0, l = ast.content.length; i < l; i++) {
|
for (let i = 0, l = ast.content.length; i < l; i++) {
|
||||||
const child = ast.content[i];
|
const child = ast.content[i];
|
||||||
const isTSet = child.type === ASTType.TSet;
|
const forceNewBlock = !child.hasNoRepresentation;
|
||||||
const subCtx = createContext(ctx, {
|
const subCtx = createContext(ctx, {
|
||||||
block,
|
block,
|
||||||
index,
|
index,
|
||||||
forceNewBlock: !isTSet,
|
forceNewBlock,
|
||||||
preventRoot: ctx.preventRoot,
|
|
||||||
isLast: ctx.isLast && i === l - 1,
|
isLast: ctx.isLast && i === l - 1,
|
||||||
});
|
});
|
||||||
this.compileAST(child, subCtx);
|
this.compileAST(child, subCtx);
|
||||||
if (!isTSet) {
|
if (forceNewBlock) {
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isNewBlock) {
|
if (isNewBlock) {
|
||||||
if (block!.hasDynamicChildren) {
|
if (block!.hasDynamicChildren && block!.children.length) {
|
||||||
if (block!.children.length) {
|
const code = this.target.code;
|
||||||
const code = this.target.code;
|
const children = block!.children.slice();
|
||||||
const children = block!.children.slice();
|
let current = children.shift();
|
||||||
let current = children.shift();
|
for (let i = codeIdx; i < code.length; i++) {
|
||||||
for (let i = codeIdx; i < code.length; i++) {
|
if (code[i].trimStart().startsWith(`const ${current!.varName} `)) {
|
||||||
if (code[i].trimStart().startsWith(`const ${current!.varName} `)) {
|
code[i] = code[i].replace(`const ${current!.varName}`, current!.varName);
|
||||||
code[i] = code[i].replace(`const ${current!.varName}`, current!.varName);
|
current = children.shift();
|
||||||
current = children.shift();
|
if (!current) break;
|
||||||
if (!current) break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.addLine(`let ${block!.children.map((c) => c.varName)};`, codeIdx);
|
|
||||||
}
|
}
|
||||||
|
this.addLine(`let ${block!.children.map((c) => c.varName).join(", ")};`, codeIdx);
|
||||||
}
|
}
|
||||||
|
|
||||||
const args = block!.children.map((c) => c.varName).join(", ");
|
const args = block!.children.map((c) => c.varName).join(", ");
|
||||||
@@ -1023,32 +1049,31 @@ export class CodeGenerator {
|
|||||||
ctxVar = generateId("ctx");
|
ctxVar = generateId("ctx");
|
||||||
this.addLine(`let ${ctxVar} = ${compileExpr(ast.context)};`);
|
this.addLine(`let ${ctxVar} = ${compileExpr(ast.context)};`);
|
||||||
}
|
}
|
||||||
|
const isDynamic = INTERP_REGEXP.test(ast.name);
|
||||||
|
const subTemplate = isDynamic ? interpolate(ast.name) : "`" + ast.name + "`";
|
||||||
|
if (block && !forceNewBlock) {
|
||||||
|
this.insertAnchor(block);
|
||||||
|
}
|
||||||
|
block = this.createBlock(block, "multi", ctx);
|
||||||
if (ast.body) {
|
if (ast.body) {
|
||||||
this.addLine(`${ctxVar} = Object.create(${ctxVar});`);
|
this.addLine(`${ctxVar} = Object.create(${ctxVar});`);
|
||||||
this.addLine(`${ctxVar}[isBoundary] = 1;`);
|
this.addLine(`${ctxVar}[isBoundary] = 1;`);
|
||||||
this.helpers.add("isBoundary");
|
this.helpers.add("isBoundary");
|
||||||
const subCtx = createContext(ctx, { preventRoot: true, ctxVar });
|
const subCtx = createContext(ctx, { ctxVar });
|
||||||
const bl = this.compileMulti({ type: ASTType.Multi, content: ast.body }, subCtx);
|
const bl = this.compileMulti({ type: ASTType.Multi, content: ast.body }, subCtx);
|
||||||
if (bl) {
|
if (bl) {
|
||||||
this.helpers.add("zero");
|
this.helpers.add("zero");
|
||||||
this.addLine(`${ctxVar}[zero] = ${bl};`);
|
this.addLine(`${ctxVar}[zero] = ${bl};`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const isDynamic = INTERP_REGEXP.test(ast.name);
|
|
||||||
const subTemplate = isDynamic ? interpolate(ast.name) : "`" + ast.name + "`";
|
const key = this.generateComponentKey();
|
||||||
if (block) {
|
|
||||||
if (!forceNewBlock) {
|
|
||||||
this.insertAnchor(block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const key = `key + \`${this.generateComponentKey()}\``;
|
|
||||||
if (isDynamic) {
|
if (isDynamic) {
|
||||||
const templateVar = generateId("template");
|
const templateVar = generateId("template");
|
||||||
if (!this.staticDefs.find((d) => d.id === "call")) {
|
if (!this.staticDefs.find((d) => d.id === "call")) {
|
||||||
this.staticDefs.push({ id: "call", expr: `app.callTemplate.bind(app)` });
|
this.staticDefs.push({ id: "call", expr: `app.callTemplate.bind(app)` });
|
||||||
}
|
}
|
||||||
this.define(templateVar, subTemplate);
|
this.define(templateVar, subTemplate);
|
||||||
block = this.createBlock(block, "multi", ctx);
|
|
||||||
this.insertBlock(`call(this, ${templateVar}, ${ctxVar}, node, ${key})`, block!, {
|
this.insertBlock(`call(this, ${templateVar}, ${ctxVar}, node, ${key})`, block!, {
|
||||||
...ctx,
|
...ctx,
|
||||||
forceNewBlock: !block,
|
forceNewBlock: !block,
|
||||||
@@ -1056,7 +1081,6 @@ export class CodeGenerator {
|
|||||||
} else {
|
} else {
|
||||||
const id = generateId(`callTemplate_`);
|
const id = generateId(`callTemplate_`);
|
||||||
this.staticDefs.push({ id, expr: `app.getTemplate(${subTemplate})` });
|
this.staticDefs.push({ id, expr: `app.getTemplate(${subTemplate})` });
|
||||||
block = this.createBlock(block, "multi", ctx);
|
|
||||||
this.insertBlock(`${id}.call(this, ${ctxVar}, node, ${key})`, block!, {
|
this.insertBlock(`${id}.call(this, ${ctxVar}, node, ${key})`, block!, {
|
||||||
...ctx,
|
...ctx,
|
||||||
forceNewBlock: !block,
|
forceNewBlock: !block,
|
||||||
@@ -1095,10 +1119,13 @@ export class CodeGenerator {
|
|||||||
} else {
|
} else {
|
||||||
let value: string;
|
let value: string;
|
||||||
if (ast.defaultValue) {
|
if (ast.defaultValue) {
|
||||||
|
const defaultValue = toStringExpression(
|
||||||
|
ctx.translate ? this.translate(ast.defaultValue, ctx.translationCtx) : ast.defaultValue
|
||||||
|
);
|
||||||
if (ast.value) {
|
if (ast.value) {
|
||||||
value = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
|
value = `withDefault(${expr}, ${defaultValue})`;
|
||||||
} else {
|
} else {
|
||||||
value = `\`${ast.defaultValue}\``;
|
value = defaultValue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value = expr;
|
value = expr;
|
||||||
@@ -1109,12 +1136,12 @@ export class CodeGenerator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateComponentKey() {
|
generateComponentKey(currentKey: string = "key") {
|
||||||
const parts = [generateId("__")];
|
const parts = [generateId("__")];
|
||||||
for (let i = 0; i < this.target.loopLevel; i++) {
|
for (let i = 0; i < this.target.loopLevel; i++) {
|
||||||
parts.push(`\${key${i + 1}}`);
|
parts.push(`\${key${i + 1}}`);
|
||||||
}
|
}
|
||||||
return parts.join("__");
|
return `${currentKey} + \`${parts.join("__")}\``;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1128,8 +1155,18 @@ export class CodeGenerator {
|
|||||||
* "some-prop" "state" "'some-prop': ctx['state']"
|
* "some-prop" "state" "'some-prop': ctx['state']"
|
||||||
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
||||||
*/
|
*/
|
||||||
formatProp(name: string, value: string): string {
|
formatProp(
|
||||||
value = this.captureExpression(value);
|
name: string,
|
||||||
|
value: string,
|
||||||
|
attrsTranslationCtx: { [name: string]: string } | null,
|
||||||
|
translationCtx: string
|
||||||
|
): string {
|
||||||
|
if (name.endsWith(".translate")) {
|
||||||
|
const attrTranslationCtx = attrsTranslationCtx?.[name] || translationCtx;
|
||||||
|
value = toStringExpression(this.translateFn(value, attrTranslationCtx));
|
||||||
|
} else {
|
||||||
|
value = this.captureExpression(value);
|
||||||
|
}
|
||||||
if (name.includes(".")) {
|
if (name.includes(".")) {
|
||||||
let [_name, suffix] = name.split(".");
|
let [_name, suffix] = name.split(".");
|
||||||
name = _name;
|
name = _name;
|
||||||
@@ -1138,17 +1175,24 @@ export class CodeGenerator {
|
|||||||
value = `(${value}).bind(this)`;
|
value = `(${value}).bind(this)`;
|
||||||
break;
|
break;
|
||||||
case "alike":
|
case "alike":
|
||||||
|
case "translate":
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new OwlError("Invalid prop suffix");
|
throw new OwlError(`Invalid prop suffix: ${suffix}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
|
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
|
||||||
return `${name}: ${value || undefined}`;
|
return `${name}: ${value || undefined}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
formatPropObject(obj: { [prop: string]: any }): string[] {
|
formatPropObject(
|
||||||
return Object.entries(obj).map(([k, v]) => this.formatProp(k, v));
|
obj: { [prop: string]: any },
|
||||||
|
attrsTranslationCtx: { [name: string]: string } | null,
|
||||||
|
translationCtx: string
|
||||||
|
): string[] {
|
||||||
|
return Object.entries(obj).map(([k, v]) =>
|
||||||
|
this.formatProp(k, v, attrsTranslationCtx, translationCtx)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPropString(props: string[], dynProps: string | null): string {
|
getPropString(props: string[], dynProps: string | null): string {
|
||||||
@@ -1165,7 +1209,9 @@ export class CodeGenerator {
|
|||||||
let { block } = ctx;
|
let { block } = ctx;
|
||||||
// props
|
// props
|
||||||
const hasSlotsProp = "slots" in (ast.props || {});
|
const hasSlotsProp = "slots" in (ast.props || {});
|
||||||
const props: string[] = ast.props ? this.formatPropObject(ast.props) : [];
|
const props: string[] = ast.props
|
||||||
|
? this.formatPropObject(ast.props, ast.propsTranslationCtx, ctx.translationCtx)
|
||||||
|
: [];
|
||||||
|
|
||||||
// slots
|
// slots
|
||||||
let slotDef: string = "";
|
let slotDef: string = "";
|
||||||
@@ -1189,7 +1235,13 @@ export class CodeGenerator {
|
|||||||
params.push(`__scope: "${scope}"`);
|
params.push(`__scope: "${scope}"`);
|
||||||
}
|
}
|
||||||
if (ast.slots[slotName].attrs) {
|
if (ast.slots[slotName].attrs) {
|
||||||
params.push(...this.formatPropObject(ast.slots[slotName].attrs!));
|
params.push(
|
||||||
|
...this.formatPropObject(
|
||||||
|
ast.slots[slotName].attrs!,
|
||||||
|
ast.slots[slotName].attrsTranslationCtx,
|
||||||
|
ctx.translationCtx
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const slotInfo = `{${params.join(", ")}}`;
|
const slotInfo = `{${params.join(", ")}}`;
|
||||||
slotStr.push(`'${slotName}': ${slotInfo}`);
|
slotStr.push(`'${slotName}': ${slotInfo}`);
|
||||||
@@ -1217,7 +1269,6 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// cmap key
|
// cmap key
|
||||||
const key = this.generateComponentKey();
|
|
||||||
let expr: string;
|
let expr: string;
|
||||||
if (ast.isDynamic) {
|
if (ast.isDynamic) {
|
||||||
expr = generateId("Comp");
|
expr = generateId("Comp");
|
||||||
@@ -1235,7 +1286,7 @@ export class CodeGenerator {
|
|||||||
this.insertAnchor(block);
|
this.insertAnchor(block);
|
||||||
}
|
}
|
||||||
|
|
||||||
let keyArg = `key + \`${key}\``;
|
let keyArg = this.generateComponentKey();
|
||||||
if (ctx.tKeyExpr) {
|
if (ctx.tKeyExpr) {
|
||||||
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
|
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
|
||||||
}
|
}
|
||||||
@@ -1308,16 +1359,17 @@ export class CodeGenerator {
|
|||||||
isMultiple = isMultiple || this.slotNames.has(ast.name);
|
isMultiple = isMultiple || this.slotNames.has(ast.name);
|
||||||
this.slotNames.add(ast.name);
|
this.slotNames.add(ast.name);
|
||||||
}
|
}
|
||||||
const dynProps = ast.attrs ? ast.attrs["t-props"] : null;
|
const attrs = { ...ast.attrs };
|
||||||
if (ast.attrs) {
|
const dynProps = attrs["t-props"];
|
||||||
delete ast.attrs["t-props"];
|
delete attrs["t-props"];
|
||||||
}
|
|
||||||
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
||||||
if (isMultiple) {
|
if (isMultiple) {
|
||||||
key = `${key} + \`${this.generateComponentKey()}\``;
|
key = this.generateComponentKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
|
const props = ast.attrs
|
||||||
|
? this.formatPropObject(attrs, ast.attrsTranslationCtx, ctx.translationCtx)
|
||||||
|
: [];
|
||||||
const scope = this.getPropString(props, dynProps);
|
const scope = this.getPropString(props, dynProps);
|
||||||
if (ast.defaultContent) {
|
if (ast.defaultContent) {
|
||||||
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
|
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
|
||||||
@@ -1350,6 +1402,15 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
compileTTranslationContext(ast: ASTTranslationContext, ctx: Context): string | null {
|
||||||
|
if (ast.content) {
|
||||||
|
return this.compileAST(
|
||||||
|
ast.content,
|
||||||
|
Object.assign({}, ctx, { translationCtx: ast.translationCtx })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
compileTPortal(ast: ASTTPortal, ctx: Context): string {
|
compileTPortal(ast: ASTTPortal, ctx: Context): string {
|
||||||
if (!this.staticDefs.find((d) => d.id === "Portal")) {
|
if (!this.staticDefs.find((d) => d.id === "Portal")) {
|
||||||
this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
|
this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
|
||||||
@@ -1357,7 +1418,6 @@ export class CodeGenerator {
|
|||||||
|
|
||||||
let { block } = ctx;
|
let { block } = ctx;
|
||||||
const name = this.compileInNewTarget("slot", ast.content, ctx);
|
const name = this.compileInNewTarget("slot", ast.content, ctx);
|
||||||
const key = this.generateComponentKey();
|
|
||||||
let ctxStr = "ctx";
|
let ctxStr = "ctx";
|
||||||
if (this.target.loopLevel || !this.hasSafeContext) {
|
if (this.target.loopLevel || !this.hasSafeContext) {
|
||||||
ctxStr = generateId("ctx");
|
ctxStr = generateId("ctx");
|
||||||
@@ -1371,7 +1431,8 @@ export class CodeGenerator {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const target = compileExpr(ast.target);
|
const target = compileExpr(ast.target);
|
||||||
const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx, Portal)`;
|
const key = this.generateComponentKey();
|
||||||
|
const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, ${key}, node, ctx, Portal)`;
|
||||||
if (block) {
|
if (block) {
|
||||||
this.insertAnchor(block);
|
this.insertAnchor(block);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import type { customDirectives } from "../common/types";
|
||||||
import type { TemplateSet } from "../runtime/template_set";
|
import type { TemplateSet } from "../runtime/template_set";
|
||||||
import type { BDom } from "../runtime/blockdom";
|
import type { BDom } from "../runtime/blockdom";
|
||||||
import { CodeGenerator, Config } from "./code_generator";
|
import { CodeGenerator, Config } from "./code_generator";
|
||||||
import { parse } from "./parser";
|
import { parse } from "./parser";
|
||||||
|
import { OwlError } from "../common/owl_error";
|
||||||
|
|
||||||
export type Template = (context: any, vnode: any, key?: string) => BDom;
|
export type Template = (context: any, vnode: any, key?: string) => BDom;
|
||||||
|
|
||||||
@@ -9,13 +11,17 @@ export type TemplateFunction = (app: TemplateSet, bdom: any, helpers: any) => Te
|
|||||||
|
|
||||||
interface CompileOptions extends Config {
|
interface CompileOptions extends Config {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
customDirectives?: customDirectives;
|
||||||
|
hasGlobalValues: boolean;
|
||||||
}
|
}
|
||||||
export function compile(
|
export function compile(
|
||||||
template: string | Element,
|
template: string | Element,
|
||||||
options: CompileOptions = {}
|
options: CompileOptions = {
|
||||||
|
hasGlobalValues: false,
|
||||||
|
}
|
||||||
): TemplateFunction {
|
): TemplateFunction {
|
||||||
// parsing
|
// parsing
|
||||||
const ast = parse(template);
|
const ast = parse(template, options.customDirectives);
|
||||||
|
|
||||||
// some work
|
// some work
|
||||||
const hasSafeContext =
|
const hasSafeContext =
|
||||||
@@ -27,5 +33,15 @@ export function compile(
|
|||||||
const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
|
const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
|
||||||
const code = codeGenerator.generateCode();
|
const code = codeGenerator.generateCode();
|
||||||
// template function
|
// template function
|
||||||
return new Function("app, bdom, helpers", code) as TemplateFunction;
|
try {
|
||||||
|
return new Function("app, bdom, helpers", code) as TemplateFunction;
|
||||||
|
} catch (originalError: any) {
|
||||||
|
const { name } = options;
|
||||||
|
const nameStr = name ? `template "${name}"` : "anonymous template";
|
||||||
|
const err = new OwlError(
|
||||||
|
`Failed to compile ${nameStr}: ${originalError.message}\n\ngenerated code:\nfunction(app, bdom, helpers) {\n${code}\n}`
|
||||||
|
);
|
||||||
|
err.cause = originalError;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OwlError } from "../runtime/error_handling";
|
import { OwlError } from "../common/owl_error";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl QWeb Expression Parser
|
* Owl QWeb Expression Parser
|
||||||
@@ -28,7 +28,7 @@ import { OwlError } from "../runtime/error_handling";
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
const RESERVED_WORDS =
|
const RESERVED_WORDS =
|
||||||
"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date".split(
|
"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date,__globals__".split(
|
||||||
","
|
","
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -268,7 +268,7 @@ export function compileExprToArray(expr: string): Token[] {
|
|||||||
const localVars = new Set<string>();
|
const localVars = new Set<string>();
|
||||||
const tokens = tokenize(expr);
|
const tokens = tokenize(expr);
|
||||||
let i = 0;
|
let i = 0;
|
||||||
let stack = []; // to track last opening [ or {
|
let stack = []; // to track last opening (, [ or {
|
||||||
|
|
||||||
while (i < tokens.length) {
|
while (i < tokens.length) {
|
||||||
let token = tokens[i];
|
let token = tokens[i];
|
||||||
@@ -279,10 +279,12 @@ export function compileExprToArray(expr: string): Token[] {
|
|||||||
switch (token.type) {
|
switch (token.type) {
|
||||||
case "LEFT_BRACE":
|
case "LEFT_BRACE":
|
||||||
case "LEFT_BRACKET":
|
case "LEFT_BRACKET":
|
||||||
|
case "LEFT_PAREN":
|
||||||
stack.push(token.type);
|
stack.push(token.type);
|
||||||
break;
|
break;
|
||||||
case "RIGHT_BRACE":
|
case "RIGHT_BRACE":
|
||||||
case "RIGHT_BRACKET":
|
case "RIGHT_BRACKET":
|
||||||
|
case "RIGHT_PAREN":
|
||||||
stack.pop();
|
stack.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { OwlError } from "../runtime/error_handling";
|
import { OwlError } from "../common/owl_error";
|
||||||
|
import type { customDirectives } from "../common/types";
|
||||||
|
import { parseXML } from "../common/utils";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// AST Type definition
|
// AST Type definition
|
||||||
@@ -25,15 +27,21 @@ export const enum ASTType {
|
|||||||
TSlot,
|
TSlot,
|
||||||
TCallBlock,
|
TCallBlock,
|
||||||
TTranslation,
|
TTranslation,
|
||||||
|
TTranslationContext,
|
||||||
TPortal,
|
TPortal,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTText {
|
export interface BaseAST {
|
||||||
|
type: ASTType;
|
||||||
|
hasNoRepresentation?: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ASTText extends BaseAST {
|
||||||
type: ASTType.Text;
|
type: ASTType.Text;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTComment {
|
export interface ASTComment extends BaseAST {
|
||||||
type: ASTType.Comment;
|
type: ASTType.Comment;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
@@ -49,11 +57,12 @@ interface TModelInfo {
|
|||||||
specialInitTargetAttr: string | null;
|
specialInitTargetAttr: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTDomNode {
|
export interface ASTDomNode extends BaseAST {
|
||||||
type: ASTType.DomNode;
|
type: ASTType.DomNode;
|
||||||
tag: string;
|
tag: string;
|
||||||
content: AST[];
|
content: AST[];
|
||||||
attrs: Attrs | null;
|
attrs: Attrs | null;
|
||||||
|
attrsTranslationCtx: Attrs | null;
|
||||||
ref: string | null;
|
ref: string | null;
|
||||||
on: EventHandlers | null;
|
on: EventHandlers | null;
|
||||||
model: TModelInfo | null;
|
model: TModelInfo | null;
|
||||||
@@ -61,24 +70,24 @@ export interface ASTDomNode {
|
|||||||
ns: string | null;
|
ns: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTMulti {
|
export interface ASTMulti extends BaseAST {
|
||||||
type: ASTType.Multi;
|
type: ASTType.Multi;
|
||||||
content: AST[];
|
content: AST[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTEsc {
|
export interface ASTTEsc extends BaseAST {
|
||||||
type: ASTType.TEsc;
|
type: ASTType.TEsc;
|
||||||
expr: string;
|
expr: string;
|
||||||
defaultValue: string;
|
defaultValue: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTOut {
|
export interface ASTTOut extends BaseAST {
|
||||||
type: ASTType.TOut;
|
type: ASTType.TOut;
|
||||||
expr: string;
|
expr: string;
|
||||||
body: AST[] | null;
|
body: AST[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTif {
|
export interface ASTTif extends BaseAST {
|
||||||
type: ASTType.TIf;
|
type: ASTType.TIf;
|
||||||
condition: string;
|
condition: string;
|
||||||
content: AST;
|
content: AST;
|
||||||
@@ -86,15 +95,16 @@ export interface ASTTif {
|
|||||||
tElse: AST | null;
|
tElse: AST | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTSet {
|
export interface ASTTSet extends BaseAST {
|
||||||
type: ASTType.TSet;
|
type: ASTType.TSet;
|
||||||
name: string;
|
name: string;
|
||||||
value: string | null; // value defined in attribute
|
value: string | null; // value defined in attribute
|
||||||
defaultValue: string | null; // value defined in body, if text
|
defaultValue: string | null; // value defined in body, if text
|
||||||
body: AST[] | null; // content of body if not text
|
body: AST[] | null; // content of body if not text
|
||||||
|
hasNoRepresentation: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTForEach {
|
export interface ASTTForEach extends BaseAST {
|
||||||
type: ASTType.TForEach;
|
type: ASTType.TForEach;
|
||||||
collection: string;
|
collection: string;
|
||||||
elem: string;
|
elem: string;
|
||||||
@@ -107,13 +117,13 @@ export interface ASTTForEach {
|
|||||||
key: string | null;
|
key: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTKey {
|
export interface ASTTKey extends BaseAST {
|
||||||
type: ASTType.TKey;
|
type: ASTType.TKey;
|
||||||
expr: string;
|
expr: string;
|
||||||
content: AST;
|
content: AST;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTCall {
|
export interface ASTTCall extends BaseAST {
|
||||||
type: ASTType.TCall;
|
type: ASTType.TCall;
|
||||||
name: string;
|
name: string;
|
||||||
body: AST[] | null;
|
body: AST[] | null;
|
||||||
@@ -125,48 +135,57 @@ interface SlotDefinition {
|
|||||||
scope: string | null;
|
scope: string | null;
|
||||||
on: EventHandlers | null;
|
on: EventHandlers | null;
|
||||||
attrs: Attrs | null;
|
attrs: Attrs | null;
|
||||||
|
attrsTranslationCtx: Attrs | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTComponent {
|
export interface ASTComponent extends BaseAST {
|
||||||
type: ASTType.TComponent;
|
type: ASTType.TComponent;
|
||||||
name: string;
|
name: string;
|
||||||
isDynamic: boolean;
|
isDynamic: boolean;
|
||||||
dynamicProps: string | null;
|
dynamicProps: string | null;
|
||||||
on: EventHandlers | null;
|
on: EventHandlers | null;
|
||||||
props: { [name: string]: string } | null;
|
props: { [name: string]: string } | null;
|
||||||
|
propsTranslationCtx: { [name: string]: string } | null;
|
||||||
slots: { [name: string]: SlotDefinition } | null;
|
slots: { [name: string]: SlotDefinition } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTSlot {
|
export interface ASTSlot extends BaseAST {
|
||||||
type: ASTType.TSlot;
|
type: ASTType.TSlot;
|
||||||
name: string;
|
name: string;
|
||||||
attrs: Attrs | null;
|
attrs: Attrs | null;
|
||||||
|
attrsTranslationCtx: Attrs | null;
|
||||||
on: EventHandlers | null;
|
on: EventHandlers | null;
|
||||||
defaultContent: AST | null;
|
defaultContent: AST | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTCallBlock {
|
export interface ASTTCallBlock extends BaseAST {
|
||||||
type: ASTType.TCallBlock;
|
type: ASTType.TCallBlock;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTDebug {
|
export interface ASTDebug extends BaseAST {
|
||||||
type: ASTType.TDebug;
|
type: ASTType.TDebug;
|
||||||
content: AST | null;
|
content: AST | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTLog {
|
export interface ASTLog extends BaseAST {
|
||||||
type: ASTType.TLog;
|
type: ASTType.TLog;
|
||||||
expr: string;
|
expr: string;
|
||||||
content: AST | null;
|
content: AST | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTranslation {
|
export interface ASTTranslation extends BaseAST {
|
||||||
type: ASTType.TTranslation;
|
type: ASTType.TTranslation;
|
||||||
content: AST | null;
|
content: AST | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTPortal {
|
export interface ASTTranslationContext extends BaseAST {
|
||||||
|
type: ASTType.TTranslationContext;
|
||||||
|
content: AST | null;
|
||||||
|
translationCtx: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ASTTPortal extends BaseAST {
|
||||||
type: ASTType.TPortal;
|
type: ASTType.TPortal;
|
||||||
target: string;
|
target: string;
|
||||||
content: AST;
|
content: AST;
|
||||||
@@ -190,6 +209,7 @@ export type AST =
|
|||||||
| ASTLog
|
| ASTLog
|
||||||
| ASTDebug
|
| ASTDebug
|
||||||
| ASTTranslation
|
| ASTTranslation
|
||||||
|
| ASTTranslationContext
|
||||||
| ASTTPortal;
|
| ASTTPortal;
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -197,30 +217,34 @@ export type AST =
|
|||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
const cache: WeakMap<Element, AST> = new WeakMap();
|
const cache: WeakMap<Element, AST> = new WeakMap();
|
||||||
|
|
||||||
export function parse(xml: string | Element): AST {
|
export function parse(xml: string | Element, customDir?: customDirectives): AST {
|
||||||
|
const ctx = {
|
||||||
|
inPreTag: false,
|
||||||
|
customDirectives: customDir,
|
||||||
|
};
|
||||||
if (typeof xml === "string") {
|
if (typeof xml === "string") {
|
||||||
const elem = parseXML(`<t>${xml}</t>`).firstChild as Element;
|
const elem = parseXML(`<t>${xml}</t>`).firstChild as Element;
|
||||||
return _parse(elem);
|
return _parse(elem, ctx);
|
||||||
}
|
}
|
||||||
let ast = cache.get(xml);
|
let ast = cache.get(xml);
|
||||||
if (!ast) {
|
if (!ast) {
|
||||||
// we clone here the xml to prevent modifying it in place
|
// we clone here the xml to prevent modifying it in place
|
||||||
ast = _parse(xml.cloneNode(true) as Element);
|
ast = _parse(xml.cloneNode(true) as Element, ctx);
|
||||||
cache.set(xml, ast);
|
cache.set(xml, ast);
|
||||||
}
|
}
|
||||||
return ast;
|
return ast;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _parse(xml: Element): AST {
|
function _parse(xml: Element, ctx: ParsingContext): AST {
|
||||||
normalizeXML(xml);
|
normalizeXML(xml);
|
||||||
const ctx = { inPreTag: false, inSVG: false };
|
|
||||||
return parseNode(xml, ctx) || { type: ASTType.Text, value: "" };
|
return parseNode(xml, ctx) || { type: ASTType.Text, value: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParsingContext {
|
interface ParsingContext {
|
||||||
tModelInfo?: TModelInfo | null;
|
tModelInfo?: TModelInfo | null;
|
||||||
|
nameSpace?: string;
|
||||||
inPreTag: boolean;
|
inPreTag: boolean;
|
||||||
inSVG: boolean;
|
customDirectives?: customDirectives;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseNode(node: Node, ctx: ParsingContext): AST | null {
|
function parseNode(node: Node, ctx: ParsingContext): AST | null {
|
||||||
@@ -228,17 +252,19 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
|
|||||||
return parseTextCommentNode(node, ctx);
|
return parseTextCommentNode(node, ctx);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
parseTCustom(node, ctx) ||
|
||||||
parseTDebugLog(node, ctx) ||
|
parseTDebugLog(node, ctx) ||
|
||||||
parseTForEach(node, ctx) ||
|
parseTForEach(node, ctx) ||
|
||||||
parseTIf(node, ctx) ||
|
parseTIf(node, ctx) ||
|
||||||
parseTPortal(node, ctx) ||
|
parseTPortal(node, ctx) ||
|
||||||
parseTCall(node, ctx) ||
|
parseTCall(node, ctx) ||
|
||||||
parseTCallBlock(node, ctx) ||
|
parseTCallBlock(node, ctx) ||
|
||||||
parseTEscNode(node, ctx) ||
|
|
||||||
parseTKey(node, ctx) ||
|
|
||||||
parseTTranslation(node, ctx) ||
|
parseTTranslation(node, ctx) ||
|
||||||
parseTSlot(node, ctx) ||
|
parseTTranslationContext(node, ctx) ||
|
||||||
|
parseTKey(node, ctx) ||
|
||||||
|
parseTEscNode(node, ctx) ||
|
||||||
parseTOutNode(node, ctx) ||
|
parseTOutNode(node, ctx) ||
|
||||||
|
parseTSlot(node, ctx) ||
|
||||||
parseComponent(node, ctx) ||
|
parseComponent(node, ctx) ||
|
||||||
parseDOMNode(node, ctx) ||
|
parseDOMNode(node, ctx) ||
|
||||||
parseTSetNode(node, ctx) ||
|
parseTSetNode(node, ctx) ||
|
||||||
@@ -276,6 +302,37 @@ function parseTextCommentNode(node: Node, ctx: ParsingContext): AST | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseTCustom(node: Element, ctx: ParsingContext): AST | null {
|
||||||
|
if (!ctx.customDirectives) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const nodeAttrsNames = node.getAttributeNames();
|
||||||
|
for (let attr of nodeAttrsNames) {
|
||||||
|
if (attr === "t-custom" || attr === "t-custom-") {
|
||||||
|
throw new OwlError("Missing custom directive name with t-custom directive");
|
||||||
|
}
|
||||||
|
if (attr.startsWith("t-custom-")) {
|
||||||
|
const directiveName = attr.split(".")[0].slice(9);
|
||||||
|
const customDirective = ctx.customDirectives[directiveName];
|
||||||
|
if (!customDirective) {
|
||||||
|
throw new OwlError(`Custom directive "${directiveName}" is not defined`);
|
||||||
|
}
|
||||||
|
const value = node.getAttribute(attr)!;
|
||||||
|
const modifiers = attr.split(".").slice(1);
|
||||||
|
node.removeAttribute(attr);
|
||||||
|
try {
|
||||||
|
customDirective(node, value, modifiers);
|
||||||
|
} catch (error) {
|
||||||
|
throw new OwlError(
|
||||||
|
`Custom directive "${directiveName}" throw the following error: ${error}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return parseNode(node, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// debugging
|
// debugging
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -283,20 +340,30 @@ function parseTextCommentNode(node: Node, ctx: ParsingContext): AST | null {
|
|||||||
function parseTDebugLog(node: Element, ctx: ParsingContext): AST | null {
|
function parseTDebugLog(node: Element, ctx: ParsingContext): AST | null {
|
||||||
if (node.hasAttribute("t-debug")) {
|
if (node.hasAttribute("t-debug")) {
|
||||||
node.removeAttribute("t-debug");
|
node.removeAttribute("t-debug");
|
||||||
return {
|
const content = parseNode(node, ctx);
|
||||||
|
const ast: ASTDebug = {
|
||||||
type: ASTType.TDebug,
|
type: ASTType.TDebug,
|
||||||
content: parseNode(node, ctx),
|
content,
|
||||||
};
|
};
|
||||||
|
if (content?.hasNoRepresentation) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.hasAttribute("t-log")) {
|
if (node.hasAttribute("t-log")) {
|
||||||
const expr = node.getAttribute("t-log")!;
|
const expr = node.getAttribute("t-log")!;
|
||||||
node.removeAttribute("t-log");
|
node.removeAttribute("t-log");
|
||||||
return {
|
const content = parseNode(node, ctx);
|
||||||
|
const ast: ASTLog = {
|
||||||
type: ASTType.TLog,
|
type: ASTType.TLog,
|
||||||
expr,
|
expr,
|
||||||
content: parseNode(node, ctx),
|
content,
|
||||||
};
|
};
|
||||||
|
if (content?.hasNoRepresentation) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -323,23 +390,23 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
if (tagName === "pre") {
|
if (tagName === "pre") {
|
||||||
ctx.inPreTag = true;
|
ctx.inPreTag = true;
|
||||||
}
|
}
|
||||||
const shouldAddSVGNS = ROOT_SVG_TAGS.has(tagName) && !ctx.inSVG;
|
|
||||||
ctx.inSVG = ctx.inSVG || shouldAddSVGNS;
|
let ns = !ctx.nameSpace && ROOT_SVG_TAGS.has(tagName) ? "http://www.w3.org/2000/svg" : null;
|
||||||
const ns = shouldAddSVGNS ? "http://www.w3.org/2000/svg" : null;
|
|
||||||
const ref = node.getAttribute("t-ref");
|
const ref = node.getAttribute("t-ref");
|
||||||
node.removeAttribute("t-ref");
|
node.removeAttribute("t-ref");
|
||||||
|
|
||||||
const nodeAttrsNames = node.getAttributeNames();
|
const nodeAttrsNames = node.getAttributeNames();
|
||||||
let attrs: ASTDomNode["attrs"] = null;
|
let attrs: ASTDomNode["attrs"] = null;
|
||||||
|
let attrsTranslationCtx: ASTDomNode["attrsTranslationCtx"] = null;
|
||||||
let on: EventHandlers | null = null;
|
let on: EventHandlers | null = null;
|
||||||
let model: TModelInfo | null = null;
|
let model: TModelInfo | null = null;
|
||||||
|
|
||||||
for (let attr of nodeAttrsNames) {
|
for (let attr of nodeAttrsNames) {
|
||||||
const value = node.getAttribute(attr)!;
|
const value = node.getAttribute(attr)!;
|
||||||
if (attr.startsWith("t-on")) {
|
if (attr === "t-on" || attr === "t-on-") {
|
||||||
if (attr === "t-on") {
|
throw new OwlError("Missing event name with t-on directive");
|
||||||
throw new OwlError("Missing event name with t-on directive");
|
}
|
||||||
}
|
if (attr.startsWith("t-on-")) {
|
||||||
on = on || {};
|
on = on || {};
|
||||||
on[attr.slice(5)] = value;
|
on[attr.slice(5)] = value;
|
||||||
} else if (attr.startsWith("t-model")) {
|
} else if (attr.startsWith("t-model")) {
|
||||||
@@ -365,13 +432,11 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
const typeAttr = node.getAttribute("type");
|
const typeAttr = node.getAttribute("type");
|
||||||
const isInput = tagName === "input";
|
const isInput = tagName === "input";
|
||||||
const isSelect = tagName === "select";
|
const isSelect = tagName === "select";
|
||||||
const isTextarea = tagName === "textarea";
|
|
||||||
const isCheckboxInput = isInput && typeAttr === "checkbox";
|
const isCheckboxInput = isInput && typeAttr === "checkbox";
|
||||||
const isRadioInput = isInput && typeAttr === "radio";
|
const isRadioInput = isInput && typeAttr === "radio";
|
||||||
const isOtherInput = isInput && !isCheckboxInput && !isRadioInput;
|
|
||||||
const hasLazyMod = attr.includes(".lazy");
|
|
||||||
const hasNumberMod = attr.includes(".number");
|
|
||||||
const hasTrimMod = attr.includes(".trim");
|
const hasTrimMod = attr.includes(".trim");
|
||||||
|
const hasLazyMod = hasTrimMod || attr.includes(".lazy");
|
||||||
|
const hasNumberMod = attr.includes(".number");
|
||||||
const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
|
const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
|
||||||
|
|
||||||
model = {
|
model = {
|
||||||
@@ -381,8 +446,8 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
specialInitTargetAttr: isRadioInput ? "checked" : null,
|
specialInitTargetAttr: isRadioInput ? "checked" : null,
|
||||||
eventType,
|
eventType,
|
||||||
hasDynamicChildren: false,
|
hasDynamicChildren: false,
|
||||||
shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
|
shouldTrim: hasTrimMod,
|
||||||
shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
|
shouldNumberize: hasNumberMod,
|
||||||
};
|
};
|
||||||
if (isSelect) {
|
if (isSelect) {
|
||||||
// don't pollute the original ctx
|
// don't pollute the original ctx
|
||||||
@@ -391,6 +456,12 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
}
|
}
|
||||||
} else if (attr.startsWith("block-")) {
|
} else if (attr.startsWith("block-")) {
|
||||||
throw new OwlError(`Invalid attribute: '${attr}'`);
|
throw new OwlError(`Invalid attribute: '${attr}'`);
|
||||||
|
} else if (attr === "xmlns") {
|
||||||
|
ns = value;
|
||||||
|
} else if (attr.startsWith("t-translation-context-")) {
|
||||||
|
const attrName = attr.slice(22);
|
||||||
|
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||||
|
attrsTranslationCtx[attrName] = value;
|
||||||
} else if (attr !== "t-name") {
|
} else if (attr !== "t-name") {
|
||||||
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
||||||
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
|
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
|
||||||
@@ -403,6 +474,9 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
attrs[attr] = value;
|
attrs[attr] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (ns) {
|
||||||
|
ctx.nameSpace = ns;
|
||||||
|
}
|
||||||
|
|
||||||
const children = parseChildren(node, ctx);
|
const children = parseChildren(node, ctx);
|
||||||
return {
|
return {
|
||||||
@@ -410,6 +484,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
tag: tagName,
|
tag: tagName,
|
||||||
dynamicTag,
|
dynamicTag,
|
||||||
attrs,
|
attrs,
|
||||||
|
attrsTranslationCtx,
|
||||||
on,
|
on,
|
||||||
ref,
|
ref,
|
||||||
content: children,
|
content: children,
|
||||||
@@ -446,9 +521,6 @@ function parseTEscNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
content: [tesc],
|
content: [tesc],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (ast.type === ASTType.TComponent) {
|
|
||||||
throw new OwlError("t-esc is not supported on Component nodes");
|
|
||||||
}
|
|
||||||
return tesc;
|
return tesc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,11 +614,19 @@ function parseTKey(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
}
|
}
|
||||||
const key = node.getAttribute("t-key")!;
|
const key = node.getAttribute("t-key")!;
|
||||||
node.removeAttribute("t-key");
|
node.removeAttribute("t-key");
|
||||||
const body = parseNode(node, ctx);
|
const content = parseNode(node, ctx);
|
||||||
if (!body) {
|
if (!content) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return { type: ASTType.TKey, expr: key, content: body };
|
const ast: ASTTKey = {
|
||||||
|
type: ASTType.TKey,
|
||||||
|
expr: key,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
if (content.hasNoRepresentation) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -572,7 +652,15 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
if (ast && ast.type === ASTType.TComponent) {
|
if (ast && ast.type === ASTType.TComponent) {
|
||||||
return {
|
return {
|
||||||
...ast,
|
...ast,
|
||||||
slots: { default: { content: tcall, scope: null, on: null, attrs: null } },
|
slots: {
|
||||||
|
default: {
|
||||||
|
content: tcall,
|
||||||
|
scope: null,
|
||||||
|
on: null,
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -660,7 +748,7 @@ function parseTSetNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
if (node.textContent !== node.innerHTML) {
|
if (node.textContent !== node.innerHTML) {
|
||||||
body = parseChildren(node, ctx);
|
body = parseChildren(node, ctx);
|
||||||
}
|
}
|
||||||
return { type: ASTType.TSet, name, value, defaultValue, body };
|
return { type: ASTType.TSet, name, value, defaultValue, body, hasNoRepresentation: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -707,9 +795,14 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
let on: ASTComponent["on"] = null;
|
let on: ASTComponent["on"] = null;
|
||||||
|
|
||||||
let props: ASTComponent["props"] = null;
|
let props: ASTComponent["props"] = null;
|
||||||
|
let propsTranslationCtx: ASTComponent["propsTranslationCtx"] = null;
|
||||||
for (let name of node.getAttributeNames()) {
|
for (let name of node.getAttributeNames()) {
|
||||||
const value = node.getAttribute(name)!;
|
const value = node.getAttribute(name)!;
|
||||||
if (name.startsWith("t-")) {
|
if (name.startsWith("t-translation-context-")) {
|
||||||
|
const attrName = name.slice(22);
|
||||||
|
propsTranslationCtx = propsTranslationCtx || {};
|
||||||
|
propsTranslationCtx[attrName] = value;
|
||||||
|
} else if (name.startsWith("t-")) {
|
||||||
if (name.startsWith("t-on-")) {
|
if (name.startsWith("t-on-")) {
|
||||||
on = on || {};
|
on = on || {};
|
||||||
on[name.slice(5)] = value;
|
on[name.slice(5)] = value;
|
||||||
@@ -741,14 +834,14 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
// be ignored)
|
// be ignored)
|
||||||
let el = slotNode.parentElement!;
|
let el = slotNode.parentElement!;
|
||||||
let isInSubComponent = false;
|
let isInSubComponent = false;
|
||||||
while (el !== clone) {
|
while (el && el !== clone) {
|
||||||
if (el!.hasAttribute("t-component") || el!.tagName[0] === el!.tagName[0].toUpperCase()) {
|
if (el!.hasAttribute("t-component") || el!.tagName[0] === el!.tagName[0].toUpperCase()) {
|
||||||
isInSubComponent = true;
|
isInSubComponent = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
el = el.parentElement!;
|
el = el.parentElement!;
|
||||||
}
|
}
|
||||||
if (isInSubComponent) {
|
if (isInSubComponent || !el) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,12 +850,17 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
const slotAst = parseNode(slotNode, ctx);
|
const slotAst = parseNode(slotNode, ctx);
|
||||||
let on: SlotDefinition["on"] = null;
|
let on: SlotDefinition["on"] = null;
|
||||||
let attrs: Attrs | null = null;
|
let attrs: Attrs | null = null;
|
||||||
|
let attrsTranslationCtx: Attrs | null = null;
|
||||||
let scope: string | null = null;
|
let scope: string | null = null;
|
||||||
for (let attributeName of slotNode.getAttributeNames()) {
|
for (let attributeName of slotNode.getAttributeNames()) {
|
||||||
const value = slotNode.getAttribute(attributeName)!;
|
const value = slotNode.getAttribute(attributeName)!;
|
||||||
if (attributeName === "t-slot-scope") {
|
if (attributeName === "t-slot-scope") {
|
||||||
scope = value;
|
scope = value;
|
||||||
continue;
|
continue;
|
||||||
|
} else if (attributeName.startsWith("t-translation-context-")) {
|
||||||
|
const attrName = attributeName.slice(22);
|
||||||
|
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||||
|
attrsTranslationCtx[attrName] = value;
|
||||||
} else if (attributeName.startsWith("t-on-")) {
|
} else if (attributeName.startsWith("t-on-")) {
|
||||||
on = on || {};
|
on = on || {};
|
||||||
on[attributeName.slice(5)] = value;
|
on[attributeName.slice(5)] = value;
|
||||||
@@ -772,7 +870,7 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
slots = slots || {};
|
slots = slots || {};
|
||||||
slots[name] = { content: slotAst, on, attrs, scope };
|
slots[name] = { content: slotAst, on, attrs, attrsTranslationCtx, scope };
|
||||||
}
|
}
|
||||||
|
|
||||||
// default slot
|
// default slot
|
||||||
@@ -780,10 +878,25 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
slots = slots || {};
|
slots = slots || {};
|
||||||
// t-set-slot="default" has priority over content
|
// t-set-slot="default" has priority over content
|
||||||
if (defaultContent && !slots.default) {
|
if (defaultContent && !slots.default) {
|
||||||
slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope };
|
slots.default = {
|
||||||
|
content: defaultContent,
|
||||||
|
on,
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
scope: defaultSlotScope,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, slots, on };
|
return {
|
||||||
|
type: ASTType.TComponent,
|
||||||
|
name,
|
||||||
|
isDynamic,
|
||||||
|
dynamicProps,
|
||||||
|
props,
|
||||||
|
propsTranslationCtx,
|
||||||
|
slots,
|
||||||
|
on,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -797,12 +910,17 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
const name = node.getAttribute("t-slot")!;
|
const name = node.getAttribute("t-slot")!;
|
||||||
node.removeAttribute("t-slot");
|
node.removeAttribute("t-slot");
|
||||||
let attrs: Attrs | null = null;
|
let attrs: Attrs | null = null;
|
||||||
|
let attrsTranslationCtx: Attrs | null = null;
|
||||||
let on: ASTComponent["on"] = null;
|
let on: ASTComponent["on"] = null;
|
||||||
for (let attributeName of node.getAttributeNames()) {
|
for (let attributeName of node.getAttributeNames()) {
|
||||||
const value = node.getAttribute(attributeName)!;
|
const value = node.getAttribute(attributeName)!;
|
||||||
if (attributeName.startsWith("t-on-")) {
|
if (attributeName.startsWith("t-on-")) {
|
||||||
on = on || {};
|
on = on || {};
|
||||||
on[attributeName.slice(5)] = value;
|
on[attributeName.slice(5)] = value;
|
||||||
|
} else if (attributeName.startsWith("t-translation-context-")) {
|
||||||
|
const attrName = attributeName.slice(22);
|
||||||
|
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||||
|
attrsTranslationCtx[attrName] = value;
|
||||||
} else {
|
} else {
|
||||||
attrs = attrs || {};
|
attrs = attrs || {};
|
||||||
attrs[attributeName] = value;
|
attrs[attributeName] = value;
|
||||||
@@ -812,20 +930,65 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
type: ASTType.TSlot,
|
type: ASTType.TSlot,
|
||||||
name,
|
name,
|
||||||
attrs,
|
attrs,
|
||||||
|
attrsTranslationCtx,
|
||||||
on,
|
on,
|
||||||
defaultContent: parseChildNodes(node, ctx),
|
defaultContent: parseChildNodes(node, ctx),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Translation
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function wrapInTTranslationAST(r: AST | null) {
|
||||||
|
const ast: ASTTranslation = { type: ASTType.TTranslation, content: r };
|
||||||
|
if (r?.hasNoRepresentation) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
|
||||||
function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
|
function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
|
||||||
if (node.getAttribute("t-translation") !== "off") {
|
if (node.getAttribute("t-translation") !== "off") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
node.removeAttribute("t-translation");
|
node.removeAttribute("t-translation");
|
||||||
return {
|
const result = parseNode(node, ctx);
|
||||||
type: ASTType.TTranslation,
|
if (result?.type === ASTType.Multi) {
|
||||||
content: parseNode(node, ctx),
|
const children = result.content.map(wrapInTTranslationAST);
|
||||||
|
return makeASTMulti(children);
|
||||||
|
}
|
||||||
|
return wrapInTTranslationAST(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Translation Context
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function wrapInTTranslationContextAST(r: AST | null, translationCtx: string) {
|
||||||
|
const ast: ASTTranslationContext = {
|
||||||
|
type: ASTType.TTranslationContext,
|
||||||
|
content: r,
|
||||||
|
translationCtx,
|
||||||
};
|
};
|
||||||
|
if (r?.hasNoRepresentation) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTTranslationContext(node: Element, ctx: ParsingContext): AST | null {
|
||||||
|
const translationCtx = node.getAttribute("t-translation-context");
|
||||||
|
if (!translationCtx) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
node.removeAttribute("t-translation-context");
|
||||||
|
const result = parseNode(node, ctx);
|
||||||
|
if (result?.type === ASTType.Multi) {
|
||||||
|
const children = result.content.map((c) => wrapInTTranslationContextAST(c, translationCtx));
|
||||||
|
return makeASTMulti(children);
|
||||||
|
}
|
||||||
|
return wrapInTTranslationContextAST(result, translationCtx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -874,6 +1037,14 @@ function parseChildren(node: Element, ctx: ParsingContext): AST[] {
|
|||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeASTMulti(children: AST[]) {
|
||||||
|
const ast: ASTMulti = { type: ASTType.Multi, content: children };
|
||||||
|
if (children.every((c) => c.hasNoRepresentation)) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse all the child nodes of a given node and return an ast if possible.
|
* Parse all the child nodes of a given node and return an ast if possible.
|
||||||
* In the case there are multiple children, they are wrapped in a astmulti.
|
* In the case there are multiple children, they are wrapped in a astmulti.
|
||||||
@@ -886,7 +1057,7 @@ function parseChildNodes(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
case 1:
|
case 1:
|
||||||
return children[0];
|
return children[0];
|
||||||
default:
|
default:
|
||||||
return { type: ASTType.Multi, content: children };
|
return makeASTMulti(children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -943,21 +1114,23 @@ function normalizeTIf(el: Element) {
|
|||||||
*
|
*
|
||||||
* @param el the element containing the tree that should be normalized
|
* @param el the element containing the tree that should be normalized
|
||||||
*/
|
*/
|
||||||
function normalizeTEsc(el: Element) {
|
function normalizeTEscTOut(el: Element) {
|
||||||
const elements = [...el.querySelectorAll("[t-esc]")].filter(
|
for (const d of ["t-esc", "t-out"]) {
|
||||||
(el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component")
|
const elements = [...el.querySelectorAll(`[${d}]`)].filter(
|
||||||
);
|
(el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component")
|
||||||
for (const el of elements) {
|
);
|
||||||
if (el.childNodes.length) {
|
for (const el of elements) {
|
||||||
throw new OwlError("Cannot have t-esc on a component that already has content");
|
if (el.childNodes.length) {
|
||||||
|
throw new OwlError(`Cannot have ${d} on a component that already has content`);
|
||||||
|
}
|
||||||
|
const value = el.getAttribute(d);
|
||||||
|
el.removeAttribute(d);
|
||||||
|
const t = el.ownerDocument.createElement("t");
|
||||||
|
if (value != null) {
|
||||||
|
t.setAttribute(d, value);
|
||||||
|
}
|
||||||
|
el.appendChild(t);
|
||||||
}
|
}
|
||||||
const value = el.getAttribute("t-esc");
|
|
||||||
el.removeAttribute("t-esc");
|
|
||||||
const t = el.ownerDocument.createElement("t");
|
|
||||||
if (value != null) {
|
|
||||||
t.setAttribute("t-esc", value);
|
|
||||||
}
|
|
||||||
el.appendChild(t);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -969,42 +1142,5 @@ function normalizeTEsc(el: Element) {
|
|||||||
*/
|
*/
|
||||||
function normalizeXML(el: Element) {
|
function normalizeXML(el: Element) {
|
||||||
normalizeTIf(el);
|
normalizeTIf(el);
|
||||||
normalizeTEsc(el);
|
normalizeTEscTOut(el);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses an XML string into an XML document, throwing errors on parser errors
|
|
||||||
* instead of returning an XML document containing the parseerror.
|
|
||||||
*
|
|
||||||
* @param xml the string to parse
|
|
||||||
* @returns an XML document corresponding to the content of the string
|
|
||||||
*/
|
|
||||||
function parseXML(xml: string): XMLDocument {
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(xml, "text/xml");
|
|
||||||
if (doc.getElementsByTagName("parsererror").length) {
|
|
||||||
let msg = "Invalid XML in template.";
|
|
||||||
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
|
|
||||||
if (parsererrorText) {
|
|
||||||
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
|
|
||||||
const re = /\d+/g;
|
|
||||||
const firstMatch = re.exec(parsererrorText);
|
|
||||||
if (firstMatch) {
|
|
||||||
const lineNumber = Number(firstMatch[0]);
|
|
||||||
const line = xml.split("\n")[lineNumber - 1];
|
|
||||||
const secondMatch = re.exec(parsererrorText);
|
|
||||||
if (line && secondMatch) {
|
|
||||||
const columnIndex = Number(secondMatch[0]) - 1;
|
|
||||||
if (line[columnIndex]) {
|
|
||||||
msg +=
|
|
||||||
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
|
|
||||||
`${line}\n${"-".repeat(columnIndex - 1)}^`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new OwlError(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// This file exports a function that allows compiling templates ahead of time.
|
||||||
|
// It is used by the "compile_owl_template" command registered in the "bin"
|
||||||
|
// section of owl's package.json
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import { readdir, readFile, stat } from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import "./setup_jsdom";
|
||||||
|
// Owl imports must be made after setting up jsdom in the global namespace
|
||||||
|
import { compile } from "..";
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// helpers
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
async function getXmlFiles(paths: string[]): Promise<string[]> {
|
||||||
|
return (
|
||||||
|
await Promise.all(
|
||||||
|
paths.map(async (file) => {
|
||||||
|
const stats = await stat(path.join(file));
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
return await getXmlFiles(
|
||||||
|
(await readdir(file)).map((fileName) => path.join(file, fileName))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (file.endsWith(".xml")) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
// adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
|
||||||
|
const a = "·-_,:;";
|
||||||
|
const p = new RegExp(a.split("").join("|"), "g");
|
||||||
|
|
||||||
|
function slugify(str: string) {
|
||||||
|
return str
|
||||||
|
.replace(/\//g, "") // remove /
|
||||||
|
.replace(/\./g, "_") // Replace . with _
|
||||||
|
.replace(p, (c) => "_") // Replace special characters
|
||||||
|
.replace(/&/g, "_and_") // Replace & with ‘and’
|
||||||
|
.replace(/[^\w\-]+/g, ""); // Remove all non-word characters
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// main
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function compileTemplates(paths: string[]) {
|
||||||
|
const files = await getXmlFiles(paths);
|
||||||
|
process.stdout.write(`Processing ${files.length} files`);
|
||||||
|
let xmlStrings = await Promise.all(files.map((file) => readFile(file, "utf8")));
|
||||||
|
|
||||||
|
const templates = [];
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const fileName = files[i];
|
||||||
|
const fileContent = xmlStrings[i];
|
||||||
|
process.stdout.write(`.`);
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(fileContent, "text/xml");
|
||||||
|
for (const template of doc.querySelectorAll("[t-name]")) {
|
||||||
|
const name = template.getAttribute("t-name");
|
||||||
|
if (template.hasAttribute("owl")) {
|
||||||
|
template.removeAttribute("owl");
|
||||||
|
}
|
||||||
|
const fnName = slugify(name!);
|
||||||
|
try {
|
||||||
|
const fn = compile(template).toString().replace("anonymous", fnName);
|
||||||
|
templates.push(`"${name}": ${fn},\n`);
|
||||||
|
} catch (e) {
|
||||||
|
errors.push({ name, fileName, e });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.stdout.write(`\n`);
|
||||||
|
|
||||||
|
for (let { name, fileName, e } of errors) {
|
||||||
|
console.warn(`Error while compiling '${name}' (in file ${fileName})`);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
console.log(`${templates.length} templates compiled`);
|
||||||
|
|
||||||
|
return `export const templates = {\n ${templates.join("\n")} \n}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import jsdom from "jsdom";
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// add global DOM stuff for compiler. Needs to be in a separate file so rollup
|
||||||
|
// doesn't hoist the owl imports above this block of code.
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
var document = new jsdom.JSDOM("", {});
|
||||||
|
var window = document.window;
|
||||||
|
global.document = window.document;
|
||||||
|
global.window = window as unknown as Window & typeof globalThis;
|
||||||
|
global.DOMParser = window.DOMParser;
|
||||||
|
global.Element = window.Element;
|
||||||
|
global.Node = window.Node;
|
||||||
@@ -12,5 +12,7 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(
|
|||||||
dev: this.dev,
|
dev: this.dev,
|
||||||
translateFn: this.translateFn,
|
translateFn: this.translateFn,
|
||||||
translatableAttributes: this.translatableAttributes,
|
translatableAttributes: this.translatableAttributes,
|
||||||
|
customDirectives: this.customDirectives,
|
||||||
|
hasGlobalValues: this.hasGlobalValues,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { version } from "../version";
|
import { version } from "../version";
|
||||||
import { Component, ComponentConstructor, Props } from "./component";
|
import { Component, ComponentConstructor, Props } from "./component";
|
||||||
import { ComponentNode } from "./component_node";
|
import { ComponentNode, saveCurrent } from "./component_node";
|
||||||
import { nodeErrorHandlers, OwlError, handleError } from "./error_handling";
|
import { nodeErrorHandlers, handleError } from "./error_handling";
|
||||||
|
import { OwlError } from "../common/owl_error";
|
||||||
import { Fiber, RootFiber, MountOptions } from "./fibers";
|
import { Fiber, RootFiber, MountOptions } from "./fibers";
|
||||||
import { Scheduler } from "./scheduler";
|
import { Scheduler } from "./scheduler";
|
||||||
import { validateProps } from "./template_helpers";
|
import { validateProps } from "./template_helpers";
|
||||||
import { TemplateSet, TemplateSetConfig } from "./template_set";
|
import { TemplateSet, TemplateSetConfig } from "./template_set";
|
||||||
import { validateTarget } from "./utils";
|
import { validateTarget } from "./utils";
|
||||||
|
import { toRaw, reactive } from "./reactivity";
|
||||||
|
|
||||||
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
|
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
|
||||||
|
|
||||||
@@ -14,24 +16,20 @@ export interface Env {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppConfig<P, E> extends TemplateSetConfig {
|
export interface RootConfig<P, E> {
|
||||||
name?: string;
|
|
||||||
props?: P;
|
props?: P;
|
||||||
env?: E;
|
env?: E;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppConfig<P, E> extends TemplateSetConfig, RootConfig<P, E> {
|
||||||
|
name?: string;
|
||||||
test?: boolean;
|
test?: boolean;
|
||||||
warnIfNoStaticProps?: boolean;
|
warnIfNoStaticProps?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasBeenLogged = false;
|
let hasBeenLogged = false;
|
||||||
|
|
||||||
export const DEV_MSG = () => {
|
const apps = new Set<App>();
|
||||||
const hash = (window as any).owl ? (window as any).owl.__info__.hash : "master";
|
|
||||||
|
|
||||||
return `Owl is running in 'dev' mode.
|
|
||||||
|
|
||||||
This is not suitable for production use.
|
|
||||||
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
|
|
||||||
};
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -39,15 +37,19 @@ declare global {
|
|||||||
apps: Set<App>;
|
apps: Set<App>;
|
||||||
Fiber: typeof Fiber;
|
Fiber: typeof Fiber;
|
||||||
RootFiber: typeof RootFiber;
|
RootFiber: typeof RootFiber;
|
||||||
|
toRaw: typeof toRaw;
|
||||||
|
reactive: typeof reactive;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.__OWL_DEVTOOLS__ ||= {
|
interface Root<P extends Props, E> {
|
||||||
apps: new Set<App>(),
|
node: ComponentNode<P, E>;
|
||||||
Fiber: Fiber,
|
mount(target: HTMLElement | ShadowRoot, options?: MountOptions): Promise<Component<P, E>>;
|
||||||
RootFiber: RootFiber,
|
destroy(): void;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
window.__OWL_DEVTOOLS__ ||= { apps, Fiber, RootFiber, toRaw, reactive };
|
||||||
|
|
||||||
export class App<
|
export class App<
|
||||||
T extends abstract new (...args: any) => any = any,
|
T extends abstract new (...args: any) => any = any,
|
||||||
@@ -55,6 +57,7 @@ export class App<
|
|||||||
E = any
|
E = any
|
||||||
> extends TemplateSet {
|
> extends TemplateSet {
|
||||||
static validateTarget = validateTarget;
|
static validateTarget = validateTarget;
|
||||||
|
static apps = apps;
|
||||||
static version = version;
|
static version = version;
|
||||||
|
|
||||||
name: string;
|
name: string;
|
||||||
@@ -62,6 +65,7 @@ export class App<
|
|||||||
props: P;
|
props: P;
|
||||||
env: E;
|
env: E;
|
||||||
scheduler = new Scheduler();
|
scheduler = new Scheduler();
|
||||||
|
subRoots: Set<ComponentNode> = new Set();
|
||||||
root: ComponentNode<P, E> | null = null;
|
root: ComponentNode<P, E> | null = null;
|
||||||
warnIfNoStaticProps: boolean;
|
warnIfNoStaticProps: boolean;
|
||||||
|
|
||||||
@@ -69,13 +73,13 @@ export class App<
|
|||||||
super(config);
|
super(config);
|
||||||
this.name = config.name || "";
|
this.name = config.name || "";
|
||||||
this.Root = Root;
|
this.Root = Root;
|
||||||
window.__OWL_DEVTOOLS__.apps.add(this);
|
apps.add(this);
|
||||||
if (config.test) {
|
if (config.test) {
|
||||||
this.dev = true;
|
this.dev = true;
|
||||||
}
|
}
|
||||||
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
|
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
|
||||||
if (this.dev && !config.test && !hasBeenLogged) {
|
if (this.dev && !config.test && !hasBeenLogged) {
|
||||||
console.info(DEV_MSG());
|
console.info(`Owl is running in 'dev' mode.`);
|
||||||
hasBeenLogged = true;
|
hasBeenLogged = true;
|
||||||
}
|
}
|
||||||
const env = config.env || {};
|
const env = config.env || {};
|
||||||
@@ -88,14 +92,49 @@ export class App<
|
|||||||
target: HTMLElement | ShadowRoot,
|
target: HTMLElement | ShadowRoot,
|
||||||
options?: MountOptions
|
options?: MountOptions
|
||||||
): Promise<Component<P, E> & InstanceType<T>> {
|
): Promise<Component<P, E> & InstanceType<T>> {
|
||||||
App.validateTarget(target);
|
const root = this.createRoot(this.Root, { props: this.props });
|
||||||
if (this.dev) {
|
this.root = root.node;
|
||||||
validateProps(this.Root, this.props, { __owl__: { app: this } });
|
this.subRoots.delete(root.node);
|
||||||
|
return root.mount(target, options) as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot<Props extends object, SubEnv = any>(
|
||||||
|
Root: ComponentConstructor<Props, E>,
|
||||||
|
config: RootConfig<Props, SubEnv> = {}
|
||||||
|
): Root<Props, SubEnv> {
|
||||||
|
const props = config.props || ({} as Props);
|
||||||
|
// hack to make sure the sub root get the sub env if necessary. for owl 3,
|
||||||
|
// would be nice to rethink the initialization process to make sure that
|
||||||
|
// we can create a ComponentNode and give it explicitely the env, instead
|
||||||
|
// of looking it up in the app
|
||||||
|
const env = this.env;
|
||||||
|
if (config.env) {
|
||||||
|
this.env = config.env as any;
|
||||||
}
|
}
|
||||||
const node = this.makeNode(this.Root, this.props);
|
|
||||||
const prom = this.mountNode(node, target, options);
|
const restore = saveCurrent();
|
||||||
this.root = node;
|
const node = this.makeNode(Root, props);
|
||||||
return prom;
|
restore();
|
||||||
|
if (config.env) {
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
this.subRoots.add(node);
|
||||||
|
return {
|
||||||
|
node,
|
||||||
|
mount: (target: HTMLElement | ShadowRoot, options?: MountOptions) => {
|
||||||
|
App.validateTarget(target);
|
||||||
|
if (this.dev) {
|
||||||
|
validateProps(Root, props, { __owl__: { app: this } });
|
||||||
|
}
|
||||||
|
const prom = this.mountNode(node, target, options);
|
||||||
|
return prom;
|
||||||
|
},
|
||||||
|
destroy: () => {
|
||||||
|
this.subRoots.delete(node);
|
||||||
|
node.destroy();
|
||||||
|
this.scheduler.processTasks();
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
|
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
|
||||||
@@ -131,10 +170,13 @@ export class App<
|
|||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if (this.root) {
|
if (this.root) {
|
||||||
this.scheduler.flush();
|
for (let subroot of this.subRoots) {
|
||||||
|
subroot.destroy();
|
||||||
|
}
|
||||||
this.root.destroy();
|
this.root.destroy();
|
||||||
|
this.scheduler.processTasks();
|
||||||
}
|
}
|
||||||
window.__OWL_DEVTOOLS__.apps.delete(this);
|
apps.delete(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
createComponent<P extends Props>(
|
createComponent<P extends Props>(
|
||||||
|
|||||||
@@ -36,10 +36,18 @@ export function createAttrUpdater(attr: string): Setter<HTMLElement> {
|
|||||||
|
|
||||||
export function attrsSetter(this: HTMLElement, attrs: any) {
|
export function attrsSetter(this: HTMLElement, attrs: any) {
|
||||||
if (isArray(attrs)) {
|
if (isArray(attrs)) {
|
||||||
setAttribute.call(this, attrs[0], attrs[1]);
|
if (attrs[0] === "class") {
|
||||||
|
setClass.call(this, attrs[1]);
|
||||||
|
} else {
|
||||||
|
setAttribute.call(this, attrs[0], attrs[1]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for (let k in attrs) {
|
for (let k in attrs) {
|
||||||
setAttribute.call(this, k, attrs[k]);
|
if (k === "class") {
|
||||||
|
setClass.call(this, attrs[k]);
|
||||||
|
} else {
|
||||||
|
setAttribute.call(this, k, attrs[k]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +60,11 @@ export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
|
|||||||
if (val === oldAttrs[1]) {
|
if (val === oldAttrs[1]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setAttribute.call(this, name, val);
|
if (name === "class") {
|
||||||
|
updateClass.call(this, val, oldAttrs[1]);
|
||||||
|
} else {
|
||||||
|
setAttribute.call(this, name, val);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
removeAttribute.call(this, oldAttrs[0]);
|
removeAttribute.call(this, oldAttrs[0]);
|
||||||
setAttribute.call(this, name, val);
|
setAttribute.call(this, name, val);
|
||||||
@@ -60,13 +72,21 @@ export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
|
|||||||
} else {
|
} else {
|
||||||
for (let k in oldAttrs) {
|
for (let k in oldAttrs) {
|
||||||
if (!(k in attrs)) {
|
if (!(k in attrs)) {
|
||||||
removeAttribute.call(this, k);
|
if (k === "class") {
|
||||||
|
updateClass.call(this, "", oldAttrs[k]);
|
||||||
|
} else {
|
||||||
|
removeAttribute.call(this, k);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let k in attrs) {
|
for (let k in attrs) {
|
||||||
const val = attrs[k];
|
const val = attrs[k];
|
||||||
if (val !== oldAttrs[k]) {
|
if (val !== oldAttrs[k]) {
|
||||||
setAttribute.call(this, k, val);
|
if (k === "class") {
|
||||||
|
updateClass.call(this, val, oldAttrs[k]);
|
||||||
|
} else {
|
||||||
|
setAttribute.call(this, k, val);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OwlError } from "../error_handling";
|
import { OwlError } from "../../common/owl_error";
|
||||||
import { attrsSetter, attrsUpdater, createAttrUpdater, setClass, updateClass } from "./attributes";
|
import { attrsSetter, attrsUpdater, createAttrUpdater, setClass, updateClass } from "./attributes";
|
||||||
import { config } from "./config";
|
import { config } from "./config";
|
||||||
import { createEventHandler } from "./events";
|
import { createEventHandler } from "./events";
|
||||||
@@ -144,12 +144,7 @@ function buildTree(
|
|||||||
info.push({ type: "child", idx: index });
|
info.push({ type: "child", idx: index });
|
||||||
el = document.createTextNode("");
|
el = document.createTextNode("");
|
||||||
}
|
}
|
||||||
const attrs = (node as Element).attributes;
|
currentNS ||= (node as Element).namespaceURI;
|
||||||
const ns = attrs.getNamedItem("block-ns");
|
|
||||||
if (ns) {
|
|
||||||
attrs.removeNamedItem("block-ns");
|
|
||||||
currentNS = ns.value;
|
|
||||||
}
|
|
||||||
if (!el) {
|
if (!el) {
|
||||||
el = currentNS
|
el = currentNS
|
||||||
? document.createElementNS(currentNS, tagName)
|
? document.createElementNS(currentNS, tagName)
|
||||||
@@ -165,6 +160,7 @@ function buildTree(
|
|||||||
const fragment = document.createElement("template").content;
|
const fragment = document.createElement("template").content;
|
||||||
fragment.appendChild(el);
|
fragment.appendChild(el);
|
||||||
}
|
}
|
||||||
|
const attrs = (node as Element).attributes;
|
||||||
for (let i = 0; i < attrs.length; i++) {
|
for (let i = 0; i < attrs.length; i++) {
|
||||||
const attrName = attrs[i].name;
|
const attrName = attrs[i].name;
|
||||||
const attrValue = attrs[i].value;
|
const attrValue = attrs[i].value;
|
||||||
|
|||||||