ref: move playground at root of folder

This commit is contained in:
Géry Debongnie
2019-03-29 09:59:39 +01:00
parent 208c7d0b63
commit 8063316f1a
1772 changed files with 0 additions and 141252 deletions
-427
View File
@@ -1,427 +0,0 @@
import h from "../libs/snabbdom/src/h";
import sdAttrs from "../libs/snabbdom/src/modules/attributes";
import sdProps from "../libs/snabbdom/src/modules/props";
import sdListeners from "../libs/snabbdom/src/modules/eventlisteners";
import { init } from "../libs/snabbdom/src/snabbdom";
import { VNode } from "../libs/snabbdom/src/vnode";
import { EventBus } from "./event_bus";
import { QWeb } from "./qweb";
import { idGenerator } from "./utils";
let getId = idGenerator();
//------------------------------------------------------------------------------
// Types/helpers
//------------------------------------------------------------------------------
export interface Env {
qweb: QWeb;
}
export interface Meta<T extends Env, Props> {
readonly id: number;
vnode: VNode | null;
isStarted: boolean;
isMounted: boolean;
isDestroyed: boolean;
parent: Component<T, any, any> | null;
children: { [key: number]: Component<T, any, any> };
// children mapping: from templateID to widgetID
// should it be a map number => Widget?
cmap: { [key: number]: number };
renderId: number;
renderProps: Props | null;
renderPromise: Promise<VNode> | null;
boundHandlers: { [key: number]: any };
}
const patch = init([sdListeners, sdAttrs, sdProps]);
export interface Type<T> extends Function {
new (...args: any[]): T;
}
//------------------------------------------------------------------------------
// Widget
//------------------------------------------------------------------------------
export class Component<
T extends Env,
Props extends {},
State extends {}
> extends EventBus {
readonly __widget__: Meta<Env, Props>;
template: string = "default";
inlineTemplate: string | null = null;
get el(): HTMLElement | null {
return this.__widget__.vnode ? (<any>this).__widget__.vnode.elm : null;
}
env: T;
state: State = <State>{};
props: Props;
refs: {
[key: string]: Component<T, any, any> | HTMLElement | undefined;
} = {};
//--------------------------------------------------------------------------
// Lifecycle
//--------------------------------------------------------------------------
/**
* Creates an instance of Component.
*
* The root widget of a component tree needs an environment:
*
* ```javascript
* const root = new RootWidget(env, props);
* ```
*
* Every other widget simply needs a reference to its parent:
*
* ```javascript
* const child = new SomeWidget(parent, props);
* ```
*
* Note that most of the time, only the root widget needs to be created by
* hand. Other widgets should be created automatically by the framework (with
* the t-widget directive in a template)
*/
constructor(parent: Component<T, any, any> | T, props?: Props) {
super();
// is this a good idea?
// Pro: if props is empty, we can create easily a widget
// Con: this is not really safe
// Pro: but creating widget (by a template) is always unsafe anyway
this.props = <Props>props || <Props>{};
let id: number = getId();
let p: Component<T, any, any> | null = null;
if (parent instanceof Component) {
p = parent;
this.env = parent.env;
parent.__widget__.children[id] = this;
} else {
this.env = parent;
}
this.__widget__ = {
id: id,
vnode: null,
isStarted: false,
isMounted: false,
isDestroyed: false,
parent: p,
children: {},
cmap: {},
renderId: 1,
renderPromise: null,
renderProps: props || null,
boundHandlers: {}
};
}
/**
* willStart is an asynchronous hook that can be implemented to perform some
* action before the initial rendering of a component.
*
* It will be called exactly once before the initial rendering. It is useful
* in some cases, for example, to load external assets (such as a JS library)
* before the widget is rendered.
*
* Note that a slow willStart method will slow down the rendering of the user
* interface. Therefore, some effort should be made to make this method as
* fast as possible.
*
* Note: this method should not be called manually.
*/
async willStart() {}
/**
* mounted is a hook that is called each time a component is attached to the
* DOM. This is a good place to add some listeners, or to interact with the
* DOM, if the component needs to perform some measure for example.
*
* Note: this method should not be called manually.
*
* @see willUnmount
*/
mounted() {}
/**
* willUnmount is a hook that is called each time a component is detached from
* the DOM. This is a good place to remove some listeners, for example.
*
* Note: this method should not be called manually.
*
* @see mounted
*/
willUnmount() {}
/**
* destroyed is a hook called exactly once, when a component is destroyed.
* When a component is destroyed, its children will be destroyed first.
*
* Note: this method should not be called manually.
*/
destroyed() {}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Attach a child widget to a given html element
*
* This is most of the time not necessary, since widgets should primarily be
* created/managed with the t-widget directive in a qweb template. However,
* for the cases where we need more control, this method will do what is
* necessary to make sure all the proper hooks are called (for example,
* mounted/willUnmount)
*
* Note that this method makes a few assumptions:
* - the child widget is indeed a child of the current widget
* - the target is inside the dom of the current widget (typically a ref)
*/
attachChild(child: Component<T, any, any>, target: HTMLElement) {
target.appendChild(child.el!);
child.__mount();
}
async mount(target: HTMLElement): Promise<void> {
const vnode = await this._start();
if (this.__widget__.isDestroyed) {
// widget was destroyed before we get here...
return;
}
this._patch(vnode);
target.appendChild(this.el!);
if (document.body.contains(target)) {
this._visitSubTree(w => {
if (!w.__widget__.isMounted && this.el!.contains(w.el)) {
w.__widget__.isMounted = true;
w.mounted();
return true;
}
return false;
});
}
}
detach() {
if (this.el) {
this._visitSubTree(w => {
if (w.__widget__.isMounted) {
w.willUnmount();
w.__widget__.isMounted = false;
return true;
}
return false;
});
this.el.remove();
}
}
async render(force: boolean = false): Promise<void> {
if (this.__widget__.isDestroyed) {
return;
}
const renderVDom = this._render(force);
const renderId = this.__widget__.renderId;
const vnode = await renderVDom;
if (renderId === this.__widget__.renderId) {
// we only update the vnode and the actual DOM if no other rendering
// occurred between now and when the render method was initially called.
this._patch(vnode);
}
}
destroy() {
if (!this.__widget__.isDestroyed) {
for (let id in this.__widget__.children) {
this.__widget__.children[id].destroy();
}
if (this.__widget__.isMounted) {
this.willUnmount();
}
if (this.el) {
this.el.remove();
this.__widget__.isMounted = false;
delete this.__widget__.vnode;
}
if (this.__widget__.parent) {
let id = this.__widget__.id;
delete this.__widget__.parent.__widget__.children[id];
this.__widget__.parent = null;
}
this.clear();
this.__widget__.isDestroyed = true;
this.destroyed();
}
}
shouldUpdate(nextProps: Props): boolean {
return true;
}
/**
* This method is the correct way to update the environment of a widget. Doing
* this will cause a full rerender of the widget and its children, so this is
* an operation that should not be done frequently.
*
* A good usecase for updating the environment would be to update some mostly
* static config keys, such as a boolean to determine if we are in mobile
* mode or not.
*/
async updateEnv(nextEnv: Partial<T>): Promise<void> {
if (this.__widget__.parent && this.__widget__.parent.env === this.env) {
this.env = Object.create(this.env);
}
Object.assign(this.env, nextEnv);
if (this.__widget__.isMounted) {
return this.render(true);
}
}
async updateProps(
nextProps: Props,
forceUpdate: boolean = false
): Promise<void> {
if (nextProps === this.__widget__.renderProps && !forceUpdate) {
await this.__widget__.renderPromise;
return;
}
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
return shouldUpdate ? this._updateProps(nextProps) : Promise.resolve();
}
/**
* This is the safest update method for widget: its job is to update the state
* and rerender (if widget is mounted).
*
* Notes:
* - it checks if we do not add extra keys to the state.
* - it is ok to call updateState before the widget is started. In that
* case, it will simply update the state and will not rerender
*/
async updateState(nextState: Partial<State>) {
if (Object.keys(nextState).length === 0) {
return;
}
Object.assign(this.state, nextState);
if (this.__widget__.isStarted) {
return this.render();
}
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
async _updateProps(nextProps: Props): Promise<void> {
this.props = nextProps;
return this.render();
}
_patch(vnode) {
this.__widget__.renderPromise = null;
this.__widget__.vnode = patch(
this.__widget__.vnode || document.createElement(vnode.sel!),
vnode
);
}
async _start(): Promise<VNode> {
this.__widget__.renderProps = this.props;
this.__widget__.renderPromise = this.willStart().then(() => {
if (this.__widget__.isDestroyed) {
return Promise.resolve(h("div"));
}
this.__widget__.isStarted = true;
if (this.inlineTemplate) {
this.env.qweb.addTemplate(
this.inlineTemplate,
this.inlineTemplate,
true
);
}
return this._render();
});
return this.__widget__.renderPromise;
}
async _render(force: boolean = false): Promise<VNode> {
this.__widget__.renderId++;
const promises: Promise<void>[] = [];
const template = this.inlineTemplate || this.template;
let vnode = this.env.qweb.render(template, this, {
promises,
handlers: this.__widget__.boundHandlers,
forceUpdate: force
});
// this part is critical for the patching process to be done correctly. The
// tricky part is that a child widget can be rerendered on its own, which
// will update its own vnode representation without the knowledge of the
// parent widget. With this, we make sure that the parent widget will be
// able to patch itself properly after
vnode.key = this.__widget__.id;
this.__widget__.renderProps = this.props;
this.__widget__.renderPromise = Promise.all(promises).then(() => vnode);
return this.__widget__.renderPromise;
}
/**
* Only called by qweb t-widget directive
*/
_mount(vnode: VNode, elm: HTMLElement): VNode {
this.__widget__.vnode = patch(elm, vnode);
this.__mount();
return this.__widget__.vnode;
}
__mount() {
if (this.__widget__.isMounted) {
return;
}
if (this.__widget__.parent) {
if (this.__widget__.parent!.__widget__.isMounted) {
this.__widget__.isMounted = true;
this.mounted();
const children = this.__widget__.children;
for (let id in children) {
children[id].__mount();
}
}
}
}
_visitSubTree(callback: (w: Component<T, any, any>) => boolean) {
const shouldVisitChildren = callback(this);
if (shouldVisitChildren) {
const children = this.__widget__.children;
for (let id in children) {
children[id]._visitSubTree(callback);
}
}
}
}
export class PureComponent<E extends Env, P, S> extends Component<E, P, S> {
shouldUpdate(nextProps: P): boolean {
for (let k in nextProps) {
if (nextProps[k] !== this.props[k]) {
return true;
}
}
return false;
}
async updateState(nextState: Partial<S>) {
for (let k in nextState) {
if (nextState[k] !== this.state[k]) {
return super.updateState(nextState);
}
}
}
}
-77
View File
@@ -1,77 +0,0 @@
/**
* We define here a simple event bus: it can
* - emit events
* - add/remove listeners.
*
* This is a useful pattern of communication in some cases.
*/
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
export type Callback = (...args: any[]) => void;
export interface Subscription {
owner: any;
callback: Callback;
}
//------------------------------------------------------------------------------
// EventBus
//------------------------------------------------------------------------------
export class EventBus {
subscriptions: { [eventType: string]: Subscription[] } = {};
/**
* Add a listener for the 'eventType' events.
*
* Note that the 'owner' of this event can be anything, but will more likely
* be a widget or a class. The idea is that the callback will be called with
* the proper owner bound.
*
* Also, the owner should be kind of unique. This will be used to remove the
* listener.
*/
on(eventType: string, owner: any, callback: Callback) {
if (!callback) {
throw new Error("Missing callback");
}
if (!this.subscriptions[eventType]) {
this.subscriptions[eventType] = [];
}
this.subscriptions[eventType].push({
owner,
callback
});
}
/**
* Remove a listener
*/
off(eventType: string, owner: any) {
const subs = this.subscriptions[eventType];
if (subs) {
this.subscriptions[eventType] = subs.filter(s => s.owner !== owner);
}
}
/**
* Emit an event of type 'eventType'. Any extra arguments will be passed to
* the listeners callback.
*/
trigger(eventType: string, ...args: any[]) {
const subs = this.subscriptions[eventType] || [];
for (let sub of subs) {
sub.callback.call(sub.owner, ...args);
}
}
/**
* Remove all subscriptions.
*/
clear() {
this.subscriptions = {};
}
}
-20
View File
@@ -1,20 +0,0 @@
import { Component, PureComponent } from "./component";
import { EventBus } from "./event_bus";
import { QWeb } from "./qweb";
import { Registry } from "./registry";
import { connect, Store } from "./store";
import * as utils from "./utils";
export const core = {
QWeb,
EventBus,
Component,
PureComponent,
utils
};
export const extras = {
Store,
connect,
Registry
};
+115
View File
@@ -0,0 +1,115 @@
import { SAMPLES } from "./samples.js";
const { QWeb, Component } = owl.core;
const MODES = {
js: "ace/mode/javascript",
css: "ace/mode/css",
xml: "ace/mode/xml"
};
const TEMPLATE = `
<div class="playground">
<div class="left-bar">
<div class="menubar">
<a class="tab" t-att-class="{active: state.currentTab==='js'}" t-on-click="setTab('js')">JS</a>
<a class="tab" t-att-class="{active: state.currentTab==='css'}" t-on-click="setTab('css')">CSS</a>
<a class="tab" t-att-class="{active: state.currentTab==='xml'}" t-on-click="setTab('xml')">XML</a>
<div class="right-thing">
<select t-on-change="setSample">
<option t-foreach="SAMPLES" t-as="sample">
<t t-esc="sample.description"/>
</option>
</select>
<a class="btn run-code" t-on-click="runCode">▶ Run</a>
</div>
</div>
<div class="code-editor" t-ref="editor"></div>
</div>
<div class="content" t-ref="content">
<div class="welcome">
<div>🦉 Odoo Web Lab 🦉</div>
<div>v<t t-esc="version"/></div>
<div class="url"><a href="https://github.com/ged-odoo/web-core">https://github.com/ged-odoo/web-core</a></div>
<div class="note">Note: these examples require a recent browser to work without a transpilation step. </div>
</div>
</div>
</div>`;
class Playground extends Component {
constructor(...args) {
super(...args);
this.version = owl._version;
this.SAMPLES = SAMPLES;
this.inlineTemplate = TEMPLATE;
this.state = {
currentTab: "js",
js: SAMPLES[0].code,
css: "",
xml: ""
};
}
mounted() {
this.editor = ace.edit(this.refs.editor);
this.editor.session.setOption("useWorker", false);
this.editor.setValue(this.state.js, -1);
this.editor.setFontSize("14px");
this.editor.setTheme("ace/theme/monokai");
this.editor.session.setMode("ace/mode/javascript");
}
runCode() {
this.updateStateFromEditor();
// inject js
const iframe = document.createElement("iframe");
iframe.src = "playground-iframe.html";
iframe.onload = () => {
const doc = iframe.contentWindow.document;
const script = doc.createElement("script");
script.type = "text/javascript";
script.innerHTML = this.state.js;
doc.body.appendChild(script);
// inject css
const link = document.createElement("link");
link.type = "text/css";
link.rel = "stylesheet";
link.innerHTML = this.state.css;
doc.body.appendChild(link);
};
this.refs.content.innerHTML = "";
this.refs.content.appendChild(iframe);
}
setSample(ev) {
const sample = SAMPLES.find(s => s.description === ev.target.value);
this.editor.setValue(sample.code, -1);
}
updateStateFromEditor() {
const value = this.editor.getValue();
this.updateState({
[this.state.currentTab]: value
});
}
setTab(tab) {
this.updateStateFromEditor();
this.editor.setValue(this.state[tab], -1);
const mode = MODES[tab];
this.editor.session.setMode(mode);
this.updateState({ currentTab: tab });
}
}
document.addEventListener("DOMContentLoaded", async function() {
const qweb = new QWeb();
const env = { qweb };
const playground = new Playground(env);
playground.mount(document.body);
});
-1002
View File
File diff suppressed because it is too large Load Diff
-28
View File
@@ -1,28 +0,0 @@
/**
* The registry is basically a simple hashmap. It is only a little safer and
* more structured than a simple object.
*/
export class Registry<T> {
private map: { [key: string]: T } = {};
/**
* Add an element to the registry. Note that the add method returns the
* registry, to it can be chained.
*/
add(key: string, item: T): Registry<T> {
if (key in this.map) {
throw new Error(`Key ${key} already exists!`);
}
this.map[key] = item;
return this;
}
/**
* Returns the element corresponding to the key
*
* Nothing is done to check that the key actually exists.
*/
get(key: string): T | undefined {
return this.map[key];
}
}
+77
View File
@@ -0,0 +1,77 @@
const HELLO_WORLD = `class HelloWorld extends owl.core.Component {
inlineTemplate = \`<div>Hello <t t-esc="props.name"/></div>\`;
}
const env = {
qweb: new owl.core.QWeb()
};
const hello = new HelloWorld(env, { name: "World" });
hello.mount(document.body);
`;
const WIDGET_COMPOSITION = `class Counter extends owl.core.Component {
inlineTemplate = \`
<div>
<button t-on-click="increment(-1)">-</button>
<span style="font-weight:bold">Value: <t t-esc="state.value"/></span>
<button t-on-click="increment(1)">+</button>
</div>\`;
constructor(parent, props) {
super(parent, props);
this.state = {
value: props.initialState || 0
};
}
increment(delta) {
this.updateState({ value: this.state.value + delta });
}
}
class App extends owl.core.Component {
inlineTemplate = \`
<div>
<t t-widget="Counter" t-props="{initialState: 1}"/>
<t t-widget="Counter" t-props="{initialState: 42}"/>
</div>\`;
widgets = { Counter };
}
const env = {
qweb: new owl.core.QWeb()
};
const app = new App(env);
app.mount(document.body);
`;
const BENCHMARK_APP = `// todo`;
const STATE_MANAGEMENT = `// todo`;
const EMPTY = ``;
export const SAMPLES = [
{
description: "Hello World",
code: HELLO_WORLD
},
{
description: "Widget Composition",
code: WIDGET_COMPOSITION
},
{
description: "Benchmark application",
code: BENCHMARK_APP
},
{
description: "State management app",
code: STATE_MANAGEMENT
},
{
description: "Empty",
code: EMPTY
}
];
-112
View File
@@ -1,112 +0,0 @@
import { EventBus } from "./event_bus";
import { Component } from "./component";
import { shallowEqual } from "./utils";
export function connect(mapStateToProps) {
return function(Comp) {
return class extends Comp {
constructor(parent, props?: any) {
const env = parent instanceof Component ? parent.env : parent;
const storeProps = mapStateToProps(env.store.state);
props = Object.assign(props || {}, storeProps);
super(parent, props);
this.__widget__.currentStoreProps = storeProps;
}
mounted() {
this.env.store.on("update", this, () => {
const storeProps = mapStateToProps(this.env.store.state);
if (!shallowEqual(storeProps, this.__widget__.currentStoreProps)) {
this.__widget__.currentStoreProps = storeProps;
// probably not optimal, will do 2 object.assign, one here and
// one in updateProps.
const nextProps = Object.assign(
{},
this.props,
this.__widget__.currentStoreProps
);
this.updateProps(nextProps, false);
}
});
}
willUnmount() {
this.env.store.off("update", this);
}
updateProps(nextProps, forceUpdate) {
nextProps = Object.assign(nextProps, this.__widget__.currentStoreProps);
return super.updateProps(nextProps, forceUpdate);
}
};
};
}
interface StoreConfig {
state?: any;
actions?: any;
mutations?: any;
}
interface StoreOption {
debug?: boolean;
}
export class Store extends EventBus {
_state: any;
actions: any;
mutations: any;
_isMutating: boolean = false;
history: any[] = [];
debug: boolean;
constructor(config: StoreConfig, options: StoreOption = {}) {
super();
this.debug = options.debug || false;
this._state = Object.assign({}, config.state);
this.actions = config.actions;
this.mutations = config.mutations;
if (this.debug) {
this.history.push({ state: this.state });
}
}
get state() {
return this._clone(this._state);
}
dispatch(action, payload) {
if (!this.actions[action]) {
throw new Error(`[Error] action ${action} is undefined`);
}
this.actions[action](
{
commit: this.commit.bind(this),
state: this.state
},
payload
);
}
async commit(type, payload) {
if (!this.mutations[type]) {
throw new Error(`[Error] mutation ${type} is undefined`);
}
this._isMutating = true;
this.mutations[type].call(null, this._state, payload);
if (this.debug) {
this.history.push({
state: this.state,
mutation: type,
payload: payload
});
}
await Promise.resolve();
if (this._isMutating) {
this._isMutating = false;
this.trigger("update", this.state);
}
}
_clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
}
-178
View File
@@ -1,178 +0,0 @@
export function escape(str: string | number | undefined): string {
if (str === undefined) {
return "";
}
if (typeof str === "number") {
return String(str);
}
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&#x27;")
.replace(/`/g, "&#x60;");
}
/**
* Remove trailing and leading spaces
*/
export function htmlTrim(s: string): string {
let result = s.replace(/(^\s+|\s+$)/g, "");
if (s[0] === " ") {
result = " " + result;
}
if (result !== " " && s[s.length - 1] === " ") {
result = result + " ";
}
return result;
}
/**
* Create a function that will generate unique id numbers
*/
export function idGenerator(): () => number {
let nextID = 1;
return () => nextID++;
}
export type HashFn = (args: any[]) => string;
export function memoize<R, T extends (...args: any[]) => R>(
f: T,
hash?: HashFn
): T {
if (!hash) {
hash = args => args.map(a => String(a)).join(",");
}
let cache: { [key: string]: R } = {};
function memoizedFunction(...args: any[]) {
let hashValue = hash!(args);
if (!(hashValue in cache)) {
cache[hashValue] = f(...args);
}
return cache[hashValue];
}
return memoizedFunction as T;
}
/**
* Returns a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
* N milliseconds. If `immediate` is passed, trigger the function on the
* leading edge, instead of the trailing.
*
* Inspired by https://davidwalsh.name/javascript-debounce-function
*/
export function debounce(
func: Function,
wait: number,
immediate?: boolean
): Function {
let timeout;
return function(this: any) {
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);
}
};
}
interface Tree<T> {
children: T[];
}
/**
* Find a node in a tree.
*
* This will traverse the tree (depth first) and return the first child that
* matches the predicate, if any
*/
export function findInTree<T extends Tree<T>>(
tree: T,
predicate: (t: T) => boolean
): T | null {
if (predicate(tree)) {
return tree;
}
for (let child of tree.children) {
let match = findInTree(child, predicate);
if (match) {
return match;
}
}
return null;
}
export function shallowEqual(objA, objB) {
if (objA === objB) {
return true;
}
const keysA = Object.keys(objA);
for (let key of keysA) {
if (!(key in objB) || objA[key] !== objB[key]) {
return false;
}
}
return true;
}
export function patch(C: any, patchName: string, patch: any) {
const proto = C.prototype;
if (!proto.__patches) {
proto.__patches = {
origMethods: {},
patches: {},
current: []
};
}
if (proto.__patches.patches[patchName]) {
throw new Error(`Patch [${patchName}] already exists`);
}
proto.__patches.patches[patchName] = patch;
applyPatch(proto, patch);
proto.__patches.current.push(patchName);
function applyPatch(proto, patch) {
Object.keys(patch).forEach(function(methodName) {
const method = patch[methodName];
if (typeof method === "function") {
const original = proto[methodName];
if (!(methodName in proto.__patches.origMethods)) {
proto.__patches.origMethods[methodName] = original;
}
proto[methodName] = function(...args) {
this._super = original;
return method.call(this, ...args);
};
}
});
}
}
export function unpatch(C: any, patchName: string) {
const proto = C.prototype;
const patchInfo = proto.__patches;
delete proto.__patches;
// reset to original
for (let k in patchInfo.origMethods) {
proto[k] = patchInfo.origMethods[k];
}
// apply other patches
for (let name of patchInfo.current) {
if (name !== patchName) {
patch(C, name, patchInfo.patches[name]);
}
}
}