/**
 * A UIPage implementation is the first 
 * Objective-UI class that is instantiated once the page loads.
 * 
 * This class is responsible for initializing the rest of the 
 * Objective-UI library and navigating to the first UIView to be displayed.
 * 
 * Here it is also possible to enable features such as Splitting, Storage, 
 * and also import native JavaScript-CSS libraries
 * 
 * UIPage will initialize a `PageShell` and act in conjunction with it 
 * to manipulate the DOM of the page as a general.
 */
export abstract class UIPage
{
    public static readonly PRODUCT_VERSION: string = '1.0.48'
    public static DISABLE_EXCEPTION_PAGE: boolean = false;
    protected mainShell: PageShell;
    public static shell: PageShell;

    public static DEBUG_MODE: boolean = false;

    public static isAppleMobileDevice(): boolean
    {
        const userAgent = window.navigator.userAgent;
        return /iPad|iPhone|iPod/.test(userAgent)
    }


    constructor(doc: Document)
    {
        this.mainShell = new PageShell(doc, this);
        UIPage.shell = this.mainShell;
        console.info(`* * * Objective-UI v${UIPage.PRODUCT_VERSION} * * *`);
    }

    protected setStorageProvider(provider: IAppStorageProvider): void
    {
        this.mainShell.setStorageProvider(provider);
    }

    protected enableSplitting(appContainerId: string, splitContainerId: string): void
    {
        this.mainShell.enableSplitting(appContainerId, splitContainerId);
    }

    protected setLibRoot(rootPath: string): void
    {
        PageShell.LIB_ROOT = rootPath;
    }

    protected importLib({ libName, cssPath, jsPath }: { libName: string; cssPath?: string; jsPath?: string; })
    {
        this.mainShell.import(new NativeLib({ libName, cssPath, jsPath }));
    }

    public navigateToView(view: UIView, preventClear: boolean = false): void
    {
        try
        {
            view.initialize(this.mainShell, preventClear);
        }
        catch (error)
        {
            new DefaultExceptionPage(error as Error);
        }
    }
}
/**
 * A UIView represents an interface view set of user controls.
 * UIView's are loaded and unloaded all the time and they 
 * house a set of Widgets that give meaning to the view in 
 * front of the user. 
 * 
 * We can say in general terms that 
 * "this is a 'screen' of the application"
 */
export abstract class UIView implements INotifiable
{
    /**
     * You must return a `ViewLayout` class instance to 
     * define the layout demarcations for this View
     * 
     * See:
     * ```
     *  class ViewLayout
     * ```
     */
    abstract buildLayout(): ViewLayout;

    /**
     * You must attach your Widgets variables with the divs 
     * contained in the Layout. 
     * Inside here, call the function `UIView.addWidgets(string, array)`
     * 
     * You **MUST NOT** manipulate the Widgets here (access attributes and functions directly), because they don't exist yet
     * 
     * Example:
     * ```
     * composeView(): void {
     *    this.addWidgets('form-div-id', this.txtName, this.txtMail);
     *    this.addWidgets('actionbar-div-id', this.btnSend, this.btnClear);
     * }
     * ``` 
     * 
     */
    abstract composeView(): void;

    /**
     * When the entire View has been rendered, 
     * you can manipulate the Widgets properties, attributes, styles 
     * and other features here.
     * 
     * You can retrieve and modify properties of divs generated by `ViewLayout` (`Row` and `Col`)
     * 
     * You can also change Widget styles through 
     * standard Widget superclass functions
     * 
     * Can load data into Widgets and do all necessary 
     * logical manipulation in your UIView from now on
     * 
     * [EXAMPLE]
     * ```
onViewDidLoad(): void
{
    //get and style a div (by id) defined in ViewLayout
    var nav = this.shellPage.elementById('nav') as HTMLDivElement;
    nav.style.background = '#007bff';
    //apply batch css to a UIHead
    this.title.applyAllCSS([
        { p: 'padding', v: '10px' },
        { p: 'color', v: 'white' }
    ])
    //apply raw-string css in a UIList
    this.sideMenu.cssFromString('margin-top: 10px;')
    //loading an array of items to the UIList
    this.sideMenu.fromList(['Customers', 'Technical Users (support)']);
}
     * ```
     */
    abstract onViewDidLoad(): void;

    /**
     * (Optional overwrite) occurs when a Widget sends a message
     */
    public onWidgetMessage(message: WidgetMessage): void
    { }

    private view: UIView;
    protected shellPage: PageShell;
    private widgetContext: WidgetContext;
    private customPresenter?: ILayoutPresenter;
    protected buildedLayout: ViewLayout;

    constructor(customLayoutPresenter?: ILayoutPresenter)
    {
        this.customPresenter = customLayoutPresenter;
    }

    /**
     * Request an instance of the Storage implementation 
     * for Session or Local storage by implementing IAppStorageProvider
     * 
     * see:
     * 
     * ```
     * interface IAppStorageProvider
     * ```
     * @param schemaName A unique id-name to determine the data scope
     */
    public requestLocalStorage(schemaName: string): AppStorage
    {
        return this.shellPage.requestStorage('local', schemaName);
    }

    public requestSessionStorage(schemaName: string): AppStorage
    {
        return this.shellPage.requestStorage('session', schemaName);
    }

    public viewContext(): WidgetContext
    {
        return this.widgetContext;
    }

    public inflateTemplateView(rawHtml: string): UITemplateView
    {
        return new UITemplateView(rawHtml, this.shellPage);
    }

    public showDialog(title: string, text: string): void
    {
        UIDialog.$ = (PageShell.BOOTSTRAP_VERSION_NUMBER >= 5 ? new UIDialogBS5(this.shellPage) : new UIDialog(this.shellPage))
            .setTitle(title)
            .setText(text)
            .action(new ModalAction({
                buttonText: 'Ok',
                dataDismiss: true
            }))
        UIDialog.$.show();
    }



    public closeDialog()
    {
        if (Misc.isNull(UIDialog.$)) return;
        UIDialog.$.closeDialog()
    }

    public createDialog(title: string): UIDialog
    {
        if (PageShell.BOOTSTRAP_VERSION_NUMBER < 5)
            UIDialog.$ = new UIDialog(this.shellPage).setTitle(title)
        else
            UIDialog.$ = new UIDialogBS5(this.shellPage).setTitle(title)

        return UIDialog.$;
    }

    public onNotified(sender: any, args: any[]): void
    {
        if (sender == 'FSWidgetContext')
            this.onViewDidLoad();
    }

    public initialize(mainShell: PageShell, preventClear: boolean = false)
    {
        UIPage.shell.loadBSVersion();


        this.shellPage = mainShell;

        this.buildedLayout = this.buildLayout();
        this.buildedLayout.render(mainShell, this.customPresenter, preventClear);

        var layoutCollection: string[] = this.buildedLayout.ElementsIdCollection();

        this.widgetContext = new WidgetContext(
            this.shellPage,
            layoutCollection,
            this.onWidgetMessage);

        this.view = this;
        this.view.composeView();
        this.widgetContext.build(this);
    }

    /**
     * Get all Widgets attached and managed in this UIView
     */
    public managedWidgets(): Array<Widget>
    {
        if (this.widgetContext == null || this.widgetContext == undefined)
            return [];
        return this.widgetContext.getManagedWidgets();
    }

    /**
     * Adds one or more Widgets to a div specified in `ViewLayout`
     * @param layoutId An 'Id' of div contained in the `ViewLayout` class
     * @param widgets An array of Widget objects that will be bound to 'layoutId'
     */
    public addWidgets(layoutId: string, ...widgets: Widget[]): UIView
    {
        for (var i = 0; i < widgets.length; i++)
            this.widgetContext.addWidget(layoutId, widgets[i]);
        return this;
    }

    /**
     * Remove a Widget managed by this UIView
     */
    protected removeWidget(widget: Widget): void
    {
        this.viewContext().removeWidget(widget);
    }

    /**
     * Finds a Widget managed by this UIView
     *
     * @param layoutId An 'Id' div contained in the `ViewLayout`class
     * @param widgetName The name of a Widget managed by this UIView and previously attached to the ViewLayout through the specified 'layoutId'
     */
    protected findWidget(layoutId: string, widgetName: string): Widget
    {
        return this.viewContext().findWidget(layoutId, widgetName);
    }

    /**
     * Create an alternative Widget Context that allows controlling 
     * a set of Widgets that are outside the main Div-app or that 
     * are in a different Div than the one used by the original 
     * Context of this UIView
     * @param managedDivIds The set of 'Id' divs managed by this Context Widget
     * @param messageProtocol A function that responds to the message triggered by this context indicating its complete loading
     */
    protected createWidgetContext(managedDivIds: string[], messageProtocol?: Function): WidgetContext
    {
        if (null == this.shellPage || undefined == this.shellPage)
            throw 'FSView.createWidgetContext(): It is not possible to do this as the View is not yet initialized. If you are making this call inside the constructor(), move it inside the composeView() function.';

        return new WidgetContext(
            this.shellPage,
            managedDivIds,
            messageProtocol
        );
    }
}
export class FlatList implements IListItemTemplateProvider
{
    private callFn: Function;
    constructor(fn: Function)
    {
        this.callFn = fn
    }
    getListItemTemplate(sender: UIList, viewModel: any): IListItemTemplate
    {
        if (Misc.isNull(viewModel)) return
        var item = new FlatListItem(viewModel)
        this.callFn(item)
        return item
    }

}

export class FlatListItem implements IListItemTemplate
{
    public value: any;
    itemName: string;
    sh: PageShell;

    constructor(vm: any)
    {
        this.value = vm;
    }

    public anchorElement: HTMLAnchorElement;

    /**  define o callback para isSelected()  */
    public onCheckSelected(fn: Function): FlatListItem
    {
        this.fn_isSelected = fn;
        return this;
    }
    private fn_isSelected: Function;

    /**  define o callback para select()*/
    public onSelect(fn: Function): FlatListItem
    {
        this.fn_select = fn;
        return this;
    }
    private fn_select: Function;
    /**  define o callback para unSelect()*/
    public onUnSelect(fn: Function): FlatListItem
    {
        this.fn_unSelect = fn;
        return this;
    }
    private fn_unSelect: Function;
    /**  define o callback para itemTemplate()*/
    public onItemTemplate(fn: Function): FlatListItem
    {
        this.fn_itemTemplate = fn;
        return this;
    }
    private fn_itemTemplate: Function;
    /**  define o um trecho html para ser usado pela funcão itemTemplate()*/
    public withHTML(htmlString: string): FlatListItem
    {
        this.fn_itemTemplateString = htmlString;
        return this;
    }

    public containsCssClass(className: string): boolean
    {
        return this.anchorElement.classList.contains(className)
    }

    public addCssClass(className: string)
    {
        this.anchorElement.classList.add(className)
    }

    public removeCssClass(className: string)
    {
        this.anchorElement.classList.remove(className)
    }

    private fn_itemTemplateString: string;


    setOwnerList(listView: UIList): void
    {
        this.sh = listView.getPageShell();
    }
    isSelected(): boolean
    {
        if (Misc.isNull(this.fn_isSelected)) return false;
        return this.fn_isSelected(this);
    }
    select(): void
    {
        if (Misc.isNull(this.fn_isSelected)) return;
        this.fn_select(this);
    }
    unSelect(): void
    {
        if (Misc.isNull(this.fn_isSelected)) return;
        this.fn_unSelect(this);
    }
    public templateView: UITemplateView

    itemTemplate(): HTMLAnchorElement
    {
        if (!Misc.isNull(this.fn_itemTemplate))
            return this.fn_itemTemplate(this);

        if (!Misc.isNull(this.fn_itemTemplateString))
        {
            const templ = new UITemplateView(this.fn_itemTemplateString, this.sh, this.value);
            this.templateView = templ
            var anchor = templ.elementById('anchor') as HTMLAnchorElement;
            this.anchorElement = anchor;
            return anchor;

        }
    }
}
export class FlatDataGrid implements IDataGridItemTemplateProvider
{
    private callFn: Function;
    constructor(fn: Function)
    {
        this.callFn = fn
    }
    getDataGridItemTemplate(sender: UIDataGrid, viewModel: any): IDataGridItemTemplate
    {
        if (Misc.isNull(viewModel)) return
        var item = new FlatDataGridItem(viewModel)
        this.callFn(item)
        return item
    }
}


export class FlatDataGridItem implements IDataGridItemTemplate
{
    public value: any;
    itemName: string;
    sh: PageShell;

    constructor(vm: any)
    {
        this.value = vm;
    }


    setOwnerDataGrid(dataGrid: UIDataGrid): void
    {
        this.sh = dataGrid.getPageShell();
    }

    public tableRow: HTMLTableRowElement;
    /**  define o callback para isSelected()  */
    public onCheckSelected(fn: Function): FlatDataGridItem
    {
        this.fn_isSelected = fn;
        return this;
    }
    private fn_isSelected: Function;

    /**  define o callback para select()*/
    public onSelect(fn: Function): FlatDataGridItem
    {
        this.fn_select = fn;
        return this;
    }
    private fn_select: Function;
    /**  define o callback para unSelect()*/
    public onUnSelect(fn: Function): FlatDataGridItem
    {
        this.fn_unSelect = fn;
        return this;
    }
    private fn_unSelect: Function;
    /**  define o callback para itemTemplate()*/
    public onItemTemplate(fn: Function): FlatDataGridItem
    {
        this.fn_itemTemplate = fn;
        return this;
    }
    private fn_itemTemplate: Function;
    /**  define o um trecho html para ser usado pela funcão itemTemplate()*/
    public withHTML(htmlString: string): FlatDataGridItem
    {
        this.fn_itemTemplateString = htmlString;
        return this;
    }

    public containsCssClass(className: string): boolean
    {
        return this.tableRow.classList.contains(className)
    }

    public addCssClass(className: string)
    {
        this.tableRow.classList.add(className)
    }

    public removeCssClass(className: string)
    {
        this.tableRow.classList.remove(className)
    }

    private fn_itemTemplateString: string;


    setOwnerList(dataGrid: UIDataGrid): void
    {
        this.sh = dataGrid.getPageShell();
    }
    isSelected(): boolean
    {
        if (Misc.isNull(this.fn_isSelected)) return false;
        return this.fn_isSelected(this);
    }
    select(): void
    {
        if (Misc.isNull(this.fn_isSelected)) return;
        this.fn_select(this);
    }
    unSelect(): void
    {
        if (Misc.isNull(this.fn_isSelected)) return;
        this.fn_unSelect(this);
    }


    itemTemplate(): HTMLTableRowElement
    {
        if (!Misc.isNull(this.fn_itemTemplate))
            return this.fn_itemTemplate(this);

        if (!Misc.isNull(this.fn_itemTemplateString))
        {
            const templ = new UITemplateView(this.fn_itemTemplateString, this.sh, this.value);
            var anchor = templ.elementById('table-row') as HTMLTableRowElement;
            this.tableRow = anchor;
            return anchor;

        }
    }


}
export abstract class UIFlatView extends UIView
{
    private static caches: ViewCache[] = [];
    private viewDictionary: ViewDictionaryEntry[] = [];


    private static findCached(path: string)
    {
        for (var c = 0; c < this.caches.length; c++)
        {
            const cached = this.caches[c];
            if (cached.path == path) return cached;
        }
        return null;
    }

    public static load(view: UIFlatView)
    {
        view.builder = view.buildView();

        const cached = (view.builder.dictionaryEnabled ? null : this.findCached(view.builder.layoutPath));
        if (!Misc.isNull(cached))
        {
            view.builder.layoutHtml = cached.content;
            UIPage.shell.navigateToView(view, view.builder.preventClear)
        }
        else
            ViewLayout.load(view.builder.layoutPath, function (html: string)
            {
                if (Misc.isNullOrEmpty(html) || html.indexOf('<title>Error</title>') > -1)
                    throw new DefaultExceptionPage(new Error(`No html-layout found for '${view.builder.layoutPath}'`))

                if (view.builder.dictionaryEnabled)
                {
                    var parser = new DOMParser();
                    var domObj = parser.parseFromString(html, "text/html");
                    var allIds = domObj.querySelectorAll('*[id]');

                    for (var i = 0; i < allIds.length; i++)
                    {
                        var element = allIds[i];
                        var currentId = element.getAttribute('id');
                        if (currentId != null)
                        {
                            var newId = `${currentId}_${Misc.generateUUID()}`;
                            view.addDictionaryEntry(currentId, newId);
                            element.setAttribute('id', newId);
                        }
                    }

                    html = domObj.getElementsByTagName('body')[0].innerHTML;
                }

                view.builder.layoutHtml = html;
                UIPage.shell.navigateToView(view, view.builder.preventClear)

                if (!view.builder.dictionaryEnabled)
                    UIFlatView.caches.push(new ViewCache(view.builder.layoutPath, html))
            });
    }

    /**
     * Allows 2+ instances of same UIFlatView 

    * @param originalId The Id of the element present in the HTML resource
    * @param generatedId The self-generated Id value
    */
    private addDictionaryEntry(originalId: string, generatedId: string)
    {
        var entry = new ViewDictionaryEntry(originalId, generatedId);
        this.viewDictionary.push(entry);
    }

    /**
     * Retrieves a physical element 'Id' registered in dictionary
     * @param originalId original element Id declared in html-layout
     * @returns fisical random element Id registered in dictionary
     */
    public dict(originalId: string): string
    {
        for (var i = 0; i < this.viewDictionary.length; i++)
        {
            const entry = this.viewDictionary[i];
            if (entry.originalId == originalId)
                return entry.managedId
        }
    }

    /**
         * Retrieves a physical element (HTMLElement-object) registered in dictionary
         * @param originalId original element Id declared in html-layout
         * @returns fisical random element Id registered in dictionary
     */
    public dictElement<TElement>(originalId: string): TElement
    {
        for (var i = 0; i < this.viewDictionary.length; i++)
        {
            const entry = this.viewDictionary[i];
            if (entry.originalId == originalId)
                return document.getElementById(entry.managedId) as TElement
        }
    }


    private builder: ViewBuilder;
    private binding: BindingContext<any | object>;
    protected abstract buildView(): ViewBuilder;

    buildLayout(): ViewLayout
    {
        return new ViewLayout(this.builder.targetId).fromHTML(this.builder.layoutHtml)
    }
    composeView(): void
    {
        for (var c = 0; c < this.builder.viewContent.length; c++)
        {
            var content: DivContent = this.builder.viewContent[c];

            if (this.builder.dictionaryEnabled)
                this.addWidgets(this.dict(content.id), ...content.w);
            else
                this.addWidgets(content.id, ...content.w);
        }
    }
    onViewDidLoad(): void
    {
        if (this.builder.hasBinding())
            this.binding = this.builder.getBinding(this);

        if (!Misc.isNull(this.builder.languageSrv))
        {
            const db = this.requestLocalStorage('i18n')
            this.translateLanguage(db.get('lang'))
        }

        this.builder.callLoadFn(this.viewContext());
    }

    protected getViewModel<TViewModel>(callValidations: boolean = true): TViewModel
    {
        return this.binding.getViewModel<TViewModel>(callValidations);
    }

    protected setViewModel<TViewModel>(instance: TViewModel, updateUI: boolean = true): void
    {
        this.binding.setViewModel(instance, updateUI);
    }

    public getBindingFor(modelPropertyName: string): WidgetBinderBehavior
    {
        return this.binding.getBindingFor(modelPropertyName);
    }

    public getBindingContext<TViewModel>(): BindingContext<TViewModel>
    {
        return this.binding;
    }

    /**
     * Causes a UI refresh on all Widgets managed by this Data Binding Context
     * based on the current values of the properties/keys of the ViewModelType instance
     * 
     * (remember that the ViewModelType instance is managed by this context as well)
     */
    public bindingRefreshUI(): void
    {
        return this.getBindingContext().refreshAll();
    }


    public translateLanguage(langName: string): void
    {
        const srv = this.builder.languageSrv

        const allWidgets = this.viewContext().getAll()
        for (var w = 0; w < allWidgets.length; w++)
        {
            const widget = allWidgets[w]
            var translation = srv.translate(widget.widgetName, langName)
            if (Misc.isNullOrEmpty(translation)) continue

            try
            {
                widget.setTitle(translation)
            } catch
            {
                try
                {
                    widget.setText(translation)
                } catch { }
            }
        }
    }
}
export class ViewBuilder
{

    public targetId: string;
    public layoutPath: string = '';
    public viewContent: DivContent[] = []
    public layoutHtml: string = null;
    private onLoadFn: Function;
    preventClear: boolean;
    dictionaryEnabled: boolean;


    private constructor(layoutPath: string)
    {
        if (Misc.isNullOrEmpty(layoutPath))
            throw new Error('UIFlatView build failed: layoutPath is required.');
        this.layoutPath = layoutPath;
    }


    private static layoutResolverFn: Function;
    public static setLayoutResolverFn(resolverFn: Function)
    {
        ViewBuilder.layoutResolverFn = resolverFn;
    }

    public static from(layoutPath: string): ViewBuilder
    {
        var path = layoutPath;
        if (!Misc.isNull(ViewBuilder.layoutResolverFn))
            path = ViewBuilder.layoutResolverFn(path);
        return new ViewBuilder(path);
    }

    public to(targetDivID: string): ViewBuilder
    {
        this.targetId = targetDivID;
        return this;
    }

    public preventClearFragment(): ViewBuilder
    {
        this.preventClear = true
        return this;
    }

    public useDictionary(): ViewBuilder
    {
        this.dictionaryEnabled = true
        return this;
    }

    private viewModelBind: any | object = null;

    public bindWith<TViewModel>(instance: TViewModel): ViewBuilder
    {
        this.viewModelBind = instance;
        return this;
    }

    private modelValidations: any[] = []
    public validate(propertyName: string, validateFn: Function): ViewBuilder
    {
        if (Misc.isNull(this.viewModelBind))
            throw new DefaultExceptionPage(new Error(`UIFlatViewBuilder: invalid call validate() function before calling bindingWith<>()`))
        this.modelValidations.push({ propertyName, validateFn })
        return this;
    }

    public hasBinding()
    {
        return Misc.isNull(this.viewModelBind) == false;
    }

    public getBinding(view: UIView): BindingContext<any | object>
    {
        const ctx = new BindingContext(this.viewModelBind, view);
        for (var v = 0; v < this.modelValidations.length; v++)
        {
            const valid = this.modelValidations[v]
            ctx.hasValidation(valid.propertyName, valid.validateFn)
        }
        return ctx;
    }

    public put(divId: string, ...w: Widget[]): ViewBuilder
    {
        this.viewContent.push(
            new DivContent(divId, ...w)
        );
        return this;
    }

    public helper(helperFn: Function): ViewBuilder
    {
        helperFn(this);
        return this;
    }

    public onLoad(fn: Function): ViewBuilder
    {
        this.onLoadFn = fn;
        return this;
    }

    callLoadFn(ctx: WidgetContext)
    {
        if (!Misc.isNull(this.onLoadFn))
            this.onLoadFn(ctx);
    }

    public languageSrv: LanguageServer = null

    public i18n(language: LanguageServer): ViewBuilder
    {
        this.languageSrv = language
        return this

    }
}
export class ViewCache
{
    public path: string = '';
    public content: string = '';

    constructor(path: string, content: string)
    {
        this.path = path;
        this.content = content;
    }
}
/**
A Widget is a TS object that represents a piece of HTML. It is able to
fetch that piece of html into a webdir and bring it to the MainPage.
of a WidgetContext and can manage several child Widgets.

It is also able to manage the elements marked with "id" attribute within that piece of HTML,
and then make them available to the inherited class as DOM objects.
 * 
 */
export abstract class Widget implements INotifiable
{

    protected abstract htmlTemplate(): string;

    /**
     * This function (in the inherited object) is triggered when "renderView()" 
     * manages to get the HTML resource from the WebDir and bind it to this Widget.
     * 
     * Within this function, it is possible to access and manipulate the DOM Elements 
     * present in the HTML resource, by calling 
     * "elementById<TElement>(string)"
     */
    protected abstract onWidgetDidLoad(): void;


    /**
     * Occurs when the Widget is detached from the WidgetContext
     */
    public onWidgetDetached(): void { throw new Error('Not implemented');}

    /**
     * Gets the default value of this widget; Note that not every Widget will implement the return of its value by this function.
     */
    public value(): any | object | string { throw new Error('Not implemented');};

    public setEnabled(enabled: boolean): void { throw new Error('Not implemented');};

    /**
     * Determines if this Widget is visible on the page
     * @param visible True or False
     */
    public setVisible(visible: boolean): void {throw new Error('Not implemented'); };

    /**
     * Add a CSS class by name; Some Widgets may not implement this eventually.
     * @param className CSS class name
     */
    public addCSSClass(className: string): void { throw new Error('Not implemented');}

    /**
     * Remove a CSS class by name; Some Widgets may not implement this eventually.
     * @param className CSS class name
     */
    public removeCSSClass(className: string): void { throw new Error('Not implemented'); }

    public setTitle(className: string): void { throw new Error('Not implemented'); }
    public setText(className: string): void { throw new Error('Not implemented'); }
    /**
     * Applies a CSS property value; Some Widgets may not implement this eventually.
     * @param propertyName CSS property name
     * @param propertyValue Property value
     */
    public applyCSS(propertyName: string, propertyValue: string): void {throw new Error('Not implemented'); }

    /**
     * Change Widget Position
     * @param position Position mode. Valid values are: 'absolute', 'relative', 'fixed', 'sticky', 'static' https://developer.mozilla.org/pt-BR/docs/Web/CSS/position
     * @param marginBottom A margin bottom value
     * @param marginLeft A margin left value
     * @param transform (optional) indicates the CSS value of 'Transform' https://developer.mozilla.org/pt-BR/docs/Web/CSS/transform
     */
    public setPosition(position: string,
        marginLeft: string,
        marginTop: string,
        marginRight: string,
        marginBottom: string,
        transform?: string): void {throw new Error('Not implemented'); }



    /**
     * 
     * @param propertyPairs Array item: [{ p: 'xxx', v: 'vvv'}, ...]
     */
    public applyAllCSS(propertyPairs: Array<any>): void
    {
        for (var i = 0; i < propertyPairs.length; i++)
        {
            var css = propertyPairs[i];
            this.applyCSS(css.p, css.v);
        }
    }

    public cssFromString(cssString: string): void
    {
        var statements = cssString.split(';');
        for (var i = 0; i < statements.length; i++)
        {
            var statement = statements[i];
            if (statement == '') continue;
            var parts = statement.split(':');
            if (parts.length == 0) continue;
            var key = parts[0].trim();
            if (key == '') continue;
            var value = parts[1].trim();
            this.applyCSS(key, value);
        }
    }

    public widgetName: string;

    private viewDictionary: ViewDictionaryEntry[];
    private DOM: Document; //DOM js object of html
    private parentFragment?: WidgetFragment;

    /**
     * 
     * @param resourceName The name of the html resource that will be fetched from the webdir and linked to this Widget
     * @param widgetName A name for this Widget instance
     */
    constructor(widgetName: string)
    {
        this.widgetName = widgetName;
        this.viewDictionary = [];
        this.DOM;
    }

    public replaceCSSClass(oldClass: string, newClass: string)
    {
        this.removeCSSClass(oldClass);
        this.addCSSClass(newClass);
    }

    public getPageShell(): PageShell
    {
        try
        {
            return this.getOwnerFragment()
                .contextRoot
                .shellPage;
        } catch (error)
        {
            throw new Error(`Attempt to access or manipulate an unattached Widget (name '${this.widgetName}'). Check if the Widget was attached during the composeView() function of the respective View that originated this call.`);
        }
    }


    /**
     * Get the fragment (page div) that this widget owns
     * @returns WidgetFragment
     */
    public getOwnerFragment(): WidgetFragment
    {
        return this.parentFragment;
    }

    /**
     * Determines the fragment (page div) that this widget owns
     */
    public setParentFragment(parentFragment: WidgetFragment): void
    {
        this.parentFragment = parentFragment;
    }

    /**
     * Sends a message from the inherited object towards the WidgetContext,
     * which then makes it available to the UIView in the "onWidgetMessage()" function call
     * @param messageId Set a default identifier for this message. This allows the receiver to determine the type of message (your widget may have some)
     * @param messageText A text for your message
     * @param messageAnyObject A custom data object
     */
    protected sendMessage(messageId: number, messageText: string, messageAnyObject: object): void
    {
        this.parentFragment?.pushMessageToRoot(this.widgetName, messageId, messageText, messageAnyObject);
    }

    protected processError(error: unknown)
    {
        new DefaultExceptionPage(error as unknown as Error);
        throw error;
    }

    /**
     * Get the Element object (DOM) respective to the entire html 
     * resource linked to the Widget 
     * 
     * If the HTML contains more than one element, you must use a DIV 
     * involving all of them and marked with an "id" attribute
     * @returns Element instance
     */
    public getDOMElement(): Element
    {
        if (this.viewDictionary.length == 0) return null;
        var firstId: string = this.viewDictionary[0].getOriginalId();
        return this.elementById(firstId);
    }


    /**
     * Gets a DOM object element from the value of the "id" attribute.
     * @param elementId Element id inside of the html template provided by inherited class
     * @returns 
     */
    protected elementById<TElement>(elementId: string): TElement
    {
        var pageShell = this.getPageShell();
        for (var i = 0; i < this.viewDictionary.length; i++)
        {
            var entry: ViewDictionaryEntry = this.viewDictionary[i];
            if (entry.getOriginalId() == elementId)
            {
                var elementResult: any = pageShell.elementById(entry.getManagedId());
                return elementResult;
            }
        }
        return null;
    }

    /**
    Adds an entry in the Id's dictionary. 
    The dictionary is used to prevent conflicting element IDs across the page.

    Before elements are attached to the page, a unique Id value is generated and (re)set 
    to the element. 

    The dictionary maintains exactly the parity of the auto-generated Id 
    with the original one, so that the inherited object can normally access 
    the elements present in the HTML resource by the original name.

     * @param originalId The Id of the element present in the HTML resource
     * @param generatedId The self-generated Id value
     */
    private addDictionaryEntry(originalId: string, generatedId: string)
    {
        var entry = new ViewDictionaryEntry(originalId, generatedId);
        this.viewDictionary.push(entry);
    }


    /**
     * This function is triggered by WidgetFragment and is responsible 
     * for use the HTML template and linking it to this Widget. 
     * 
     * From here, all elements present in the HTML marked with some "Id" 
     * attribute will be made availableas DOM Elements to the inherited object when 
     * "onWidgetDidLoad()" is invoked
     * @param onloadNotifiable An Notifiable to receive a notification when the Widget is rendered
     */
    public renderView(onloadNotifiable: INotifiable)
    {
        var self = this;

        this.viewDictionary = [];

        var html: string = this.htmlTemplate();

        if (Misc.isNullOrEmpty(html))
            new Error(`Cannot render a Widget named '${this.widgetName}' because the html-template is empty. Ensure that function htmlTemplate() returns a valid html string.`)

        var parser = new DOMParser();
        var domObj = parser.parseFromString(html, "text/html");
        var allIds = domObj.querySelectorAll('*[id]');

        for (var i = 0; i < allIds.length; i++)
        {
            var element = allIds[i];
            var currentId = element.getAttribute('id');
            if (currentId != null)
            {
                var newId = `${currentId}_${Misc.generateUUID()}`;
                self.addDictionaryEntry(currentId, newId);
                element.setAttribute('id', newId);
            }
        }

        self.DOM = domObj;

        var child: ChildNode = domObj.documentElement.childNodes[1].firstChild;

        if (UIPage.DEBUG_MODE)
        {
            var lb = document.createElement('label');
            lb.textContent = `Widget: ${this.widgetName}`
            child.appendChild(lb);
        }

        self.parentFragment.appendChildElementToContainer(child as Element);

        UIPage.shell.loadBSVersion();

        self.onWidgetDidLoad();
        onloadNotifiable.onNotified('FSWidget', [self, domObj]);
    }


    onNotified(sender: any, args: any[]): void { }


    /**
     * @deprecated Now, call this function from "Misc" class. Ex.:
     * ```
     * var uid = Misc.generateUUID();
     * ```
     */
    public static generateUUID(): string
    {
        return Misc.generateUUID();
    }
}
/**
 * A WidgetContext is able to manage a 
 * set of widgets linked in a div
 * contained in a `ViewLayout`
 * 
 * This is automatically managed by the UIView, 
 * but new WidgetContext's can be dynamically 
 * created to manage another portion of Widgets 
 * located in other Divs.
 */
export class WidgetContext implements INotifiable
{

    fragments: WidgetFragment[];
    messageProtocolFunction?: Function;
    fragmentsLoaded: number;
    notifiableView?: INotifiable; //based on FSView
    shellPage: PageShell;
    ctx: WidgetContext;
    contextLoaded: boolean = false;

    constructor(shellPage: PageShell,
        managedElementsIds: string[],
        messageProtocolFunction?: Function)
    {
        this.fragments = [];
        this.messageProtocolFunction = messageProtocolFunction;

        var self = this;
        self.shellPage = shellPage;

        for (var i = 0; i < managedElementsIds.length; i++)
        {
            var elementId = managedElementsIds[i];
            var divElement = shellPage.elementById(elementId) as HTMLDivElement;
            self.fragments.push(new WidgetFragment(self, divElement));
        }
    }
    onNotified(sender: any, args: any[]): void
    {

    }
    contextShell(): PageShell
    {
        return this.shellPage;
    }

    findFragment(fragmentName: string): WidgetFragment
    {
        for (var i = 0; i < this.fragments.length; i++)
        {
            var fragment: WidgetFragment = this.fragments[i];
            if (fragment.fragmentId == fragmentName)
                return fragment;
        }
        return null
    }

    findWidget(fragmentName: string, widgetName: string)
    {
        var fragment: WidgetFragment = this.findFragment(fragmentName);
        var widget: Widget = fragment.findWidget(widgetName);
        return widget;
    }

    get<TWidget>(path: string): TWidget
    {
        const fragmentName: string = path.split('/')[0];
        const widgetName: string = path.split('/')[1];
        var fragment: WidgetFragment = this.findFragment(fragmentName);
        var widget: Widget = fragment.findWidget(widgetName);
        return widget as unknown as TWidget;
    }

    gets(fragmentName: string): Widget[]
    {
        var fragment: WidgetFragment = this.findFragment(fragmentName);
        return fragment.widgets
    }

    getAll(): Widget[]
    {
        var widgets: Widget[] = [];
        for (var i = 0; i < this.fragments.length; i++)
        {
            var fragment: WidgetFragment = this.fragments[i];
          widgets.push(...fragment.widgets);
        }
        return widgets
    }


    pushMessage(widgetName: string, messageId: number, messageText: string, messageAnyObject: object)
    {
        if (this.messageProtocolFunction != null)
        {
            this.messageProtocolFunction(
                new WidgetMessage(
                    widgetName,
                    messageId,
                    messageText,
                    messageAnyObject
                )
            );
        }
    }

    /**
     * Attaches a Widget to a `WidgetFragment`.
     * A `WidgetFragment` is the direct controller of ONE 
     * div and can manage multiple Widgets related to this div
     */
    addWidget(fragmentName: string, widget: Widget)
    {
        var fragment = this.findFragment(fragmentName)
        if (Misc.isNull(fragment))
            throw new Error(`Cannot add a Widget named '${widget.widgetName}' to WidgetFragment '${fragmentName}'. Ensure that layout-html contains a div with Id="${fragmentName}"`)

        fragment.attatchWidget(widget);

        if (this.contextLoaded)
        {
            let last = fragment.widgets.length - 1;
            fragment.widgets[last].renderView(this);
        }

        return this;
    }

    getManagedWidgets(): Array<Widget>
    {
        try
        {
            var widgets: Array<Widget> = [];
            for (var frg = 0; frg < this.fragments.length; frg++)
            {
                var fragment: WidgetFragment = this.fragments[frg];
                for (var wdg = 0; wdg < fragment.widgets.length; wdg++)
                {
                    var widget: Widget = fragment.widgets[wdg];
                    widgets.push(widget);
                }
            }
            return widgets;
        }
        catch (e)
        {
            return [];
        }
    }

    removeWidget(widget: Widget)
    {
        if (widget == null) return;
        var fragment = widget.getOwnerFragment();
        if (fragment == null) return;
        fragment.dettatchwidget(widget);
    }

    onFragmentLoad()
    {
        this.fragmentsLoaded += 1;
        if (this.fragmentsLoaded == this.fragments.length)
        {
            this.contextLoaded = true;
            if (this.notifiableView != null)
                this.notifiableView.onNotified('FSWidgetContext', []);
        }
    }

    clear()
    {
        for (var i = 0; i < this.fragments.length; i++)
        {
            var fragment: WidgetFragment = this.fragments[i];
            fragment.clear();
        }
    }

    /**
     * Performs the rendering of the Widgets attached to this Context.
     * Immediately orders the Fragments managed by this Context to draw 
     * the Widgets they manage.
     * @param notifiable 
     */
    build(notifiable?: INotifiable, clear: boolean = false)
    {
        this.notifiableView = notifiable;
        if (this.contextLoaded)
        {
            if (this.notifiableView != null)
                this.notifiableView.onNotified('FSWidgetContext', []);
            return;
        }

        this.fragmentsLoaded = 0;

        for (var i = 0; i < this.fragments.length; i++)
        {
            var fragment: WidgetFragment = this.fragments[i];
            if (clear == true)
                fragment.clear();

            fragment.renderFragmentwidgets();
        }
    }
}
/**
 * An efficient system of data binding and object synchronization (aka 'ViewModel') 
 * with the User Interface
 * 
 */

export class BindingContext<ViewModel>
{
    public toString(): string
    {
        return '[BINDING-CONTEXT]';
    }

    private _binders: Array<WidgetBinder> = [];
    private viewModelInstance: ViewModel;

    /**
     * This is a concrete class and you should instantiate it normally,
     * You must provide an instance of the ViewModel and the inherited UIView currently displayed. 
     * But ATTENTION you must do this INSIDE the onViewDidload() function of your UIView inherited class.
     * 
     * ```
     *  export class MyView extends UIView { 
     *     private binding: BindingContext<ModelType>; 
     *     ... 
     *     onViewDidload(): void { 
     *        //Here Widgets attached in UIView will be linked with `ModelType`
     *        this.binding = new BindingContext<ModelType>(new ModelType(), this); 
     *        ...
     *     }
     * ```
     * @param viewModel An instance of the ViewModel object
     * @param view UIView instance inherits class (the currently displayed UIView)
     */
    constructor(viewModel: ViewModel, view: UIView)
    {
        this.viewModelInstance = viewModel;
        this.scanViewModel(view);
    }

    /**
     * 
     * @param modelPropertyName 
     * @param validateFn 
     ```
        function(propVal: any) {
            // check value
            // apply UI changes
            // return true|false;
        }
     ```
     */
    public hasValidation(modelPropertyName: string, validateFn: Function)
    {
        const binder = this.getBinder(modelPropertyName);
        if (Misc.isNull(binder))
            throw new DefaultExceptionPage(new Error(`BindingContext<${typeof (this.viewModelInstance)}> : not found a WidgetBinder for model property '${modelPropertyName}'`));

        binder.addValidation(validateFn);
    }

    private getBinder(modelPropertyName: string): WidgetBinder
    {
        for (var i = 0; i < this._binders.length; i++)
        {
            var binder: WidgetBinder = this._binders[i];
            if (binder.modelPropertyName == modelPropertyName)
                return binder;
        }
    }

    /**
     * Gets a WidgetBinderBehavior from which the behavior of data bindings will be changed.
     * @param modelPropertyName The name of the property/key present in the ViewModelType type
     * @returns `WidgetBinderBehavior`
     */
    public getBindingFor(modelPropertyName: string): WidgetBinderBehavior
    {
        var propBinders: Array<WidgetBinder> = [];
        for (var i = 0; i < this._binders.length; i++)
        {
            var binder: WidgetBinder = this._binders[i];
            if (binder.modelPropertyName == modelPropertyName)
                propBinders.push(binder);
        }
        return new WidgetBinderBehavior(propBinders);
    }

    /**
     * Causes a UI refresh on all Widgets managed by this Data Binding Context
     * based on the current values of the properties/keys of the ViewModelType instance \
     * \
     * (remember that the ViewModelType instance is managed by this context as well)
     */
    public refreshAll(): void
    {
        for (var b = 0; b < this._binders.length; b++)
        {
            var binder: WidgetBinder = this._binders[b];
            binder.refreshUI();
        }
    }


    /**
     * Causes a UI refresh on a single Widget managed by this Data Binding Context
     * based on the current values of the properties/keys of the ViewModelType instance \
     * \
     * (remember that the ViewModelType instance is managed by this context as well)
     */
    public refreshSingle(name: string): void
    {
        for (var b = 0; b < this._binders.length; b++)
        {
            var binder: WidgetBinder = this._binders[b];
            if (binder.modelPropertyName == name)
                binder.refreshUI();
        }
    }

    /**
     * Causes a UI refresh on a these Widget's managed by this Data Binding Context
     * based on the current values of the properties/keys of the ViewModelType instance \
     * \
     * (remember that the ViewModelType instance is managed by this context as well)
     */
    public refreshThese(...names: string[]): void
    {
        for (var b = 0; b < this._binders.length; b++)
        {
            var binder: WidgetBinder = this._binders[b];
            for (var i = 0; i < names.length; i++)
            {
                if (binder.modelPropertyName == names[i])
                    binder.refreshUI();
            }

        }
    }

    /**
     * Get an instance of `ViewModel` based on Widgets values
     * @returns `ViewModel`
     */
    public getViewModel<ViewModel>(callValidations: boolean = true): ViewModel
    {
        for (var i = 0; i < this._binders.length; i++)
        {
            const binder = this._binders[i]
            binder.fillPropertyModel();

            if (callValidations)
            {
                if (binder.hasValidation())
                    if (!binder.validate())
                        return null;
            }


        }
        return this.viewModelInstance as unknown as ViewModel;
    }

    /**
     * Defines an instance of `ViewModel`.\
     * This causes an immediate UI refresh on all widgets managed by this context. \
     * \
     * You can also use this to reset (say 'clear') the Widgets state by passing a `new ViewModel()`
     * @param viewModelInstance `ViewModel` 
     * @returns 
     */

    public setViewModel(viewModelInstance: ViewModel, updateUI: boolean = true): BindingContext<ViewModel>
    {
        this.viewModelInstance = viewModelInstance;

        if (updateUI)
        {
            for (var b = 0; b < this._binders.length; b++)
            {
                var binder: WidgetBinder = this._binders[b];
                binder.setModel(this.viewModelInstance, binder.modelPropertyName);
            }
            this.refreshAll();
        }
        return this;
    }

    /**
     * Scans the Widgets managed in a UIView for matches with 
     * properties/keys present in the ViewModel type object 
     * managed by this Context
     */
    private scanViewModel(view: UIView): void
    {
        var self = this;
        var widgets: Array<Widget> = view.managedWidgets();
        if (widgets == null || widgets == undefined || widgets.length == 0)
            throw new Error("Illegal declaration: BindingContext cannot be initialized by the View's constructor. Consider instantiating it in onViewDidLoad()");

        for (var key in self.viewModelInstance)
        {
            for (var w = 0; w < widgets.length; w++)
            {
                var widget: Widget = widgets[w];
                var keyMatch: boolean = self.isModelPropertyMatchWithWidget(widget, key);
                if (keyMatch)
                    this.bindWidget(widget, key);
            }
        }
    }

    private isModelPropertyMatchWithWidget(widget: Widget, modelKey: string): boolean
    {
        var widgetName: string = widget.widgetName;
        if (widgetName.indexOf(modelKey) < 0) return false;
        var replaced: string = widgetName.replace(modelKey, '');
        var propLength: number = modelKey.length;
        var replacedLength: number = replaced.length;
        return (replacedLength < propLength);
    }

    private bindWidget(widget: Widget, modelKey: string): WidgetBinder
    {
        try
        {
            var binder: WidgetBinder = (widget as unknown as IBindable).getBinder();
            if (binder == null || binder == undefined)
                return null;

            binder.setModel(this.viewModelInstance, modelKey);
            this._binders.push(binder);
            return binder;
        } catch
        {
            return null;
        }
    }
}
/**
 * Implementing this interface will make a
 * Widget compatible with Data Binding features
 */
export interface IBindable
{
    /**
     * Produces an instance of the `WidgetBinder` implementation. 
     * This class is responsible for controlling the data binding 
     * flow for this Widget
     */
    getBinder(): WidgetBinder;
}
/**
 *  Allows you to define binding behaviors for a 
 * set of `WidgetBinder` 
 * classes that are bound to a property/key of 
 * the object-model being managed by the `BindingContext<T>`
 */
export class WidgetBinderBehavior
{
    private _binders: Array<WidgetBinder>;
    constructor(binders: Array<WidgetBinder>)
    {
        this._binders = binders;
    }

    /**
     * When data binding is done based on a list of model objects, 
     * it may be necessary to specify a binding path INSIDE that model 
     * object via its properties/keys.
     * 
     * This happens for example when binding a 
     * list of `'Contact'` in `UISelect` or `UIList`: 
     * although they are able to load an Array<Contact>, 
     * they won't know that they should use the prop/key 'Id' 
     * like selection value and the 'Name' prop/key as 
     * the display value.
     * @param displayPropertyName The prop/key on the model object that will be displayed in the control
     * @param valuePropertyName The prop/key in the model object that will be used as the selected value in the Widget
     */
    hasPath(displayPropertyName: string, valuePropertyName: string): WidgetBinderBehavior
    {
        for (var i = 0; i < this._binders.length; i++)
            this._binders[i].hasPath(displayPropertyName, valuePropertyName);
        return this;
    }

    /**
     * Set a target when you want the selected Widget 
     * value to be transferred to a given property/key 
     * in the same model-object.
     * @param targetValuePropertyName  Property/key in the model-class to which the selected Widget-value will be transferred.
     */
    hasTarget(targetValuePropertyName: string): WidgetBinderBehavior
    {
        for (var i = 0; i < this._binders.length; i++)
            this._binders[i].hasTarget(targetValuePropertyName);
        return this;
    }
}
/**
 * It acts as a bridge between the `BindingContext<T>` 
 * and the respective Widget. 
 * 
 * This allows the Widget to incorporate Data Binding 
 * functionality with model-objects.
 * 
 * If you have a custom `Widget` created in your project, 
 * you will need to provide a `WidgetBinder` implementation 
 * to provide Data Binding functionality
 */
export abstract class WidgetBinder
{

    abstract getWidgetValue(): any | object;
    abstract refreshUI(): void;
    abstract fillPropertyModel(): void;

    protected widget: Widget;
    public widgetName: string;
    private bindingName: string;

    private _viewModel: any | object;
    public modelPropertyName: string;
    public modelTargetProperty?: string;

    public bindingHasPath: boolean;
    public displayProperty: string;
    public valueProperty: string;

    private validateFn: Function;

    constructor(widget: Widget) 
    {
        this.widget = widget;
        this.widgetName = widget.widgetName;
        this.bindingName = `${typeof (widget)}Binding ${this.widgetName} => ${typeof (widget)}`;
    }

    addValidation(validateFn: Function)
    {
        this.validateFn = validateFn;
    }

    public hasValidation()
    {
        return !Misc.isNull(this.validateFn);
    }

    public validate(): boolean
    {
        var val = this.getModelPropertyValue()
        var result = this.validateFn(val)
        if (Misc.isNull(result))
            throw new DefaultExceptionPage(new Error(`WidgetBinder: invalid result of validation function for property '${this.modelPropertyName}'. Check if validation function contains a 'return true|false' instruction. `));

        if (result == true || result == false)
            return result as boolean;

        throw new DefaultExceptionPage(new Error(`WidgetBinder: invalid result of validation function for property '${this.modelPropertyName}'. Check if validation function contains a 'return true|false' instruction. `));
    }

    getModelPropertyValue(): any | object
    {
        if (this._viewModel == null || this.modelPropertyName == null || this.modelPropertyName == '')
            return null;
        var value = this._viewModel[this.modelPropertyName];
        return value;
    }

    setModelPropertyValue(value: any | object): void
    {
        if (this._viewModel == null || this.modelPropertyName == null || this.modelPropertyName == '')
            return;

        var mValue = this.getModelPropertyValue()
        if (typeof mValue == 'number')
            value = parseFloat(value)
        if (typeof mValue == 'boolean')
            value = (`${value}`.toLocaleLowerCase() == 'true' ? true : false)

        if (`${value}` == 'NaN')
            value = 0

        this._viewModel[this.modelPropertyName] = value;
    }

    toString(): string
    {
        return this.bindingName;
    }

    hasPath(displayPropertyName: string, valuePropertyName: string): WidgetBinder
    {
        this.bindingHasPath = true;
        this.displayProperty = displayPropertyName;
        this.valueProperty = valuePropertyName;
        this.refreshUI();
        return this;
    }

    hasTarget(targetValuePropertyName: string): WidgetBinder
    {
        this.modelTargetProperty = targetValuePropertyName;
        this.refreshUI();
        return this;
    }

    isTargetDefined(): boolean
    {
        return this.modelTargetProperty != null;
    }

    fillModelTargetPropertyValue(): void
    {
        if (this.isTargetDefined() == false) return;
        var value = this.getWidgetValue();

        var mValue = this.getModelTargetPropertyValue()
        if (typeof mValue == 'number')
            value = parseFloat(value);
        if (typeof mValue == 'boolean')
            value = (`${value}`.toLocaleLowerCase() == 'true' ? true : false)
        if (`${value}` == 'NaN')
            value = 0

        this._viewModel[this.modelTargetProperty] = value;
    }

    setModelTargetPropertyValue(value: any | object)
    {
        this._viewModel[this.modelTargetProperty] = value;
    }

    getModelTargetPropertyValue(): any | object
    {
        if (this.isTargetDefined() == false) return;
        var value = this._viewModel[this.modelTargetProperty];
        return value;
    }

    setModel(viewModelInstance: any | object, propertyName: string): void
    {
        this._viewModel = viewModelInstance;
        this.modelPropertyName = propertyName;
        this.bindingName = `${typeof (this.widget)}Binding ${this.widgetName} => ${typeof (this.widget)}.${this.modelPropertyName}`;

        this.refreshUI();
    }

    getViewModel<TModel>(): TModel
    {
        return this._viewModel as TModel
    }
}
export class Bearer
{
    public static get(token: string): Headers
    {
        return new Headers({
            'content-type': 'application/json',
            'authorization': `Bearer ${token}`
        });
    }
}

export class APIResponse
{
    public statusCode: number;
    public statusMessage: string;
    public content: any | object;

    constructor({ code, msg, content }:
        {
            code: number,
            msg: string,
            content: any | object,
        })
    {
        this.statusCode = code;
        this.statusMessage = msg;
        this.content = content;
    }
}

/**
 * Offers an abstraction for consuming REST APIs with the 
 * possibility of simulating a local 
 * API for development purposes;
 * 
 * If your FrontEnd is not running alongside the API, 
 * be careful to call previously (once) 
 * ```
 * WebAPI.setURLBase('https://complete-url-of-api.com')
 * ``` 
 * 
 * Example: 
 * ```
WebAPI
.POST('/api/route/xyz') //.GET() / .PUT() / .DELETE()
.withBody(objectBodyHere) //if .POST() or .PUT()
.onSuccess(function (res: APIResponse){ 
    //success handle function
})
.onError(function(err: Error){
    //request-error handle function
})
.call(); //call REST api
 * ```
 */
export class WebAPI
{
    public static urlBase: string;
    private contentType: string = 'application/json';
    public static setURLBase(apiUrlBase: string)
    {
        WebAPI.urlBase = apiUrlBase;
    }

    public static requestTo(requestString: string, httpMethod: string)
    {
        if (requestString.startsWith('http'))
            return new WebAPI(requestString, httpMethod);
        else
        {
            if (this.urlBase == '' || this.urlBase == undefined)
                return new WebAPI(`${requestString}`, httpMethod);
            else
                return new WebAPI(`${WebAPI.urlBase}${requestString}`, httpMethod);
        }
    }

    public static useSimulator(simulator: WebAPISimulator): void
    {
        WebAPI.simulator = simulator;
    }

    public static GET(requestString: string)
    {
        return this.requestTo(requestString, 'GET');
    }

    public static POST(requestString: string)
    {

        return this.requestTo(requestString, 'POST');
    }

    public static PUT(requestString: string)
    {
        return this.requestTo(requestString, 'PUT');
    }

    public static DELETE(requestString: string)
    {
        return this.requestTo(requestString, 'DELETE');
    }

    private request: RequestInit;
    private apiUrl: string;
    private fnOnSuccess: Function;
    private fnDataResultTo: Function;
    private fnOnError: Function;
    private static simulator: WebAPISimulator;

    private constructor(url: string, method: string)
    {
        this.request = {};
        this.request.method = method;
        this.apiUrl = url;
        this.withHeaders(new Headers({'content-type': this.contentType  }));
    }

    public call(): void
    {
        if (WebAPI.simulator == null)
        {
            var statusCode: number;
            var statusMsg: string;
            var self = this;
            if(Misc.isNull(self.request.headers)) self.withHeaders(new Headers({'content-type': self.contentType  }));
            fetch(self.apiUrl, self.request)
                .then(function (ret: Response)
                {
                    statusCode = ret.status;
                    statusMsg = ret.statusText;
                    return ret.text();
                })
                .then(function (text: string)
                {
                    var responseContent: any | object = null;
                    if (text.startsWith("{") || text.startsWith("["))
                        responseContent = JSON.parse(text);
                    else
                        responseContent = text

                    if (statusCode == 400)
                    {
                        for (var prop in responseContent.errors)
                        {
                            statusMsg += responseContent.errors[prop]
                        }
                    }
                    var apiResponse = new APIResponse({
                        code: statusCode, msg: statusMsg, content: responseContent
                    });

                    return apiResponse;
                })
                .then(function (res: APIResponse)
                {
                    const code = res.statusCode;
                    if (code == 200 || code == 201 || code == 202 || code == 203 || code == 204)
                    {
                        if (self.fnOnSuccess != null)
                            self.fnOnSuccess(res);
                        if (self.fnDataResultTo != null)
                        {
                            var data = res.content;
                            self.fnDataResultTo(data);
                        }
                    }
                    else
                    {
                        if (self.fnOnError != null)
                            self.fnOnError(res)
                    }

                })
                .catch(err => (self.fnOnError == null ? {} : self.fnOnError(err)));
        }
        else
        {
            try
            {
                var result = WebAPI.simulator.simulateRequest(
                    this.request.method,
                    this.apiUrl.replace(WebAPI.urlBase, ''),
                    this.request.body);

                if (Misc.isNull(this.fnOnSuccess) == false)
                    this.fnOnSuccess(result);

                if (Misc.isNullOrEmpty(this.fnDataResultTo) == false)
                {
                    var data = result.content;
                    this.fnDataResultTo(data);
                }
            } catch (error)
            {
                this.fnOnError(error);
            }
        }
    }

    public setContentType(contentType: string): WebAPI
    {
        this.contentType = contentType;
         this.withHeaders(new Headers({'content-type': this.contentType  }));
        return this;
    }

    public dataResultTo(callBack: Function): WebAPI
    {
        this.fnDataResultTo = callBack;
        return this;
    }

    public onSuccess(callBack: Function): WebAPI
    {
        this.fnOnSuccess = callBack;
        return this;
    }

    public onError(callBack: Function): WebAPI
    {
        this.fnOnError = callBack;
        return this;
    }

    public withAllOptions(requestInit: RequestInit): WebAPI
    {
        this.request = requestInit;
        return this;
    }

    public withBody(requestBody: any | object): WebAPI
    {
        this.request.body = JSON.stringify(requestBody);
        return this;
    }

    public withHeaders(headers: Headers): WebAPI
    {
        this.request.headers = headers;
        return this;
    }
}
export class SimulatedAPIRoute
{
    private method: string;
    private resource: string;
    private endPoint: Function;

    constructor(resource: string, method: string, endPoint: Function)
    {
        this.method = method;
        this.resource = resource;
        this.endPoint = endPoint;
    }

    public getMethod()
    {
        return this.method;
    }

    public getResource()
    {
        const parIndx: number = this.resource.indexOf('{');
        if (parIndx > 0)
        {
            var pureRes = this.resource.substring(0,parIndx)
            return pureRes;
        }

        return this.resource;
    }

    public simulateRoute({ body = null, params = null }:
        {
            body?: any | object,
            params?: Array<string>
        }): any
    {
        if (this.method == 'GET' || this.method == 'DELETE')
        {
            return this.endPoint(params);
        }
        else
            return this.endPoint(body);
    }

    public toString()
    {
        return `[${this.method}] ${this.resource}`;
    }
}

/**
 * Allows you to simulate a REST API locally based on an 
 * API that may not yet exist, but will respond for the routes
 * that are defined in the Simulator.
 * 
 * You must inherit this class and define it in
    ```
    WebAPI.useSimulator(new MyAPISimulatorImpl());
    ```
 */
export abstract class WebAPISimulator
{
    private simulatedRoutes: Array<SimulatedAPIRoute> = [];

    /**
     * Maps a simulated route to which this 
     * Simulator should respond when 
     * ```
     *WebAPI.call()
     * ``` 
     * is invoked
     * 
     * @param httpMethod 'GET' / 'POST' / 'PUT' / 'DELETE'
     * @param resource The resource name or endpoint path that the real API would have
     * @param endPoint A function (callback) that should respond for the resource endpoint 
     *
     * Function definition should follow these standards:
     * 
     * **for GET/DELETE routes** - 
     *  ```
     * functionEndpointName(params: Array<string>): any|object
     * ```
     *                   
     * **for POST/PUT routes** 
     * ```
     * functionEndpointName(body: any|object): any|object
     * ```
     */
    protected mapRoute(httpMethod: string, resource: string, endPoint: Function): WebAPISimulator
    {
        this.simulatedRoutes.push(new SimulatedAPIRoute(resource, httpMethod, endPoint));
        return this;
    }

    /**
     * Fires a request originating from the `WebAPI` 
     * class and redirected to the Simulator,
     * which will respond by calling 
     * a "fake-endpoint" function
     */
    public simulateRequest(
        httpMethod: string,
        resource: string,
        body: any | object): APIResponse
    {
        for (var i = 0; i < this.simulatedRoutes.length; i++)
        {
            const route: SimulatedAPIRoute = this.simulatedRoutes[i];
            const isResource = resource.startsWith(route.getResource());
            const isMethod = httpMethod == route.getMethod();

            if (isResource && isMethod)
            {
                if (route.getMethod() == 'GET' || route.getMethod() == 'DELETE')
                {
                    const path = resource.replace(route.getResource(), '');
                    var params = path.split('/');
                    if (params.length > 0)
                        if (params[0] == '')
                            params.shift();

                    return new APIResponse({
                        code: 200,
                        msg: 'fetched from API Simulator',
                        content: route.simulateRoute({ params })
                    });
                }

                if (route.getMethod() == 'POST' || route.getMethod() == 'PUT')
                {
                    return new APIResponse({
                        code: 200,
                        msg: 'fetched from API Simulator',
                        content: route.simulateRoute({ body: JSON.parse(body) })
                    })
                }
                break;
            }
        }

        return new APIResponse({
            code: 404,
            msg: `[API SIMULATOR] Resource '${resource}' not found; Check if route is correctly typed; Ensure your simulated api functions is maped on constructor;`,
            content: null
        });
    }
}
/**
 * Initialization options for div-columns
 */
export class ColOptions
{
    colClass?: string;
    colHeight?: string;
    rows?: Row[];
}


/**
 * Represents a Column-Div with standard Bootstrap classes and a height of 100px
 */
export class Col
{
    public toString(): string
    {
        return 'BT-COLUMN';
    }

    id: string;
    colClass?: string = 'col-lg-12 col-md-12 col-sm-12 col-sm-12';
    colHeight?: string = '100px';
    columnRows?: Row[] = [];

    /**
     * 
     * @param id The 'Id' attribute that the resulting div will have
     * @param options 
     */
    constructor(id: string, options?: ColOptions)
    {
        this.id = id;

        if (options != null)
        {
            if (Misc.isNullOrEmpty(options.colHeight) == false)
                this.colHeight = options.colHeight;

            if (Misc.isNullOrEmpty(options.colClass) == false)
                this.colClass = options.colClass;

            if (options.rows !== null)
                this.columnRows = options.rows;
        }
    }
}
/**
 * A standard implementation for `ILayoutPresenter`
 */
export class DefaultLayoutPresenter implements ILayoutPresenter
{
    presenter: DefaultLayoutPresenter;
    private pageShell: PageShell;

    constructor()
    {
        this.presenter = this;
    }


    renderLayout(layout: ViewLayout, pageShell: PageShell): Element
    {
        this.pageShell = pageShell;
        var parentContainer: HTMLDivElement = layout.containerDivObj as HTMLDivElement;

        if (parentContainer == null) return null;



        parentContainer.innerHTML = '';
        // parentContainer.style.opacity = '0';


        if (UIPage.DEBUG_MODE)
        {
            parentContainer.append(`
                ViewLayout: ${parentContainer.id}
            `);
        }

        for (let rowIndex = 0; rowIndex < layout.layoutRows.length; rowIndex++)
        {
            var rowObj: Row = layout.layoutRows[rowIndex];
            var rowDivEl = this.renderRow(rowObj) as HTMLDivElement;

            if(UIPage.DEBUG_MODE)
               rowDivEl. append(`
                Row: ${rowDivEl.id}
            `);

            parentContainer.appendChild(rowDivEl);
        }

        return parentContainer;
    }

    private renderRow(row: Row): HTMLDivElement
    {

        //creates master row div
        const rowDiv: HTMLDivElement = document.createElement("div");
        if (row.rowClass != null && row.rowClass != undefined)
        {
            const classes: Array<string> = row.rowClass.split(' ');
            for (var i = 0; i < classes.length; i++)
            {
                const className = classes[i].trim();
                if (className == '') continue;
                rowDiv.classList.add(className);
            }
        }

        rowDiv.id = row.id;
        rowDiv.style.height = row.rowHeidth;

        if (row.rowColumns != null)
        {
            for (let index = 0; index < row.rowColumns.length; index++)
            {
                const column: Col = row.rowColumns[index];

                //an sub-div column
                const colDiv: HTMLDivElement = document.createElement("div");

                if (Misc.isNullOrEmpty(column.colClass) == false)
                {
                    const classes: Array<string> = column.colClass.split(' ');
                    for (var i = 0; i < classes.length; i++)
                    {
                        const className = classes[i].trim();
                        if (className == '') continue;
                        colDiv.classList.add(className);
                    }
                }

                colDiv.id = column.id;
                colDiv.style.height = column.colHeight;

                if(UIPage.DEBUG_MODE)
                    colDiv.append(`
                        Col: ${colDiv.id}
                    `);

                rowDiv.appendChild(colDiv);

                if (column.columnRows != null)
                {
                    for (let subRowIndex = 0; subRowIndex < column.columnRows.length; subRowIndex++)
                    {
                        //sub-div column has rows
                        const columnSubRow = column.columnRows[subRowIndex];

                        //recursivelly call renderRow() again,
                        const subRowElement = this.renderRow(columnSubRow);

                        //then catch Element result and append it to sub-div column (aka "colDiv")

                        if (subRowElement != null)
                            colDiv.appendChild(subRowElement);
                    }
                }
            }
        }
        return rowDiv;
    }
}
/**
 * Implementing this interface in a common 
 * class will allow an isolated point of 
 * customization of a given Widget
 * 
 * You should transfer the instance of this 
 * implementation to the `setCustomPresenter()` 
 * function in the `Widget` object
 */
export interface ICustomWidgetPresenter<TWidget>
{
    render(widget: TWidget) : void;
}
/**
 * A layout presenter's role is to read and 
 * transform into reality (DOM Div's components) the data 
 * of the `ViewLayout` class whose instance is provided 
 * by the `UIView` inherited class
 */
export interface ILayoutPresenter {
    renderLayout(layout: ViewLayout,  pageShell: PageShell):Element;
}
/**
 * Represents a notifiable object. Initially it is used by the 
 * WidgetContext to notify data to the UIView. \
 * But it can be used generically for any purpose.
 */
export interface INotifiable
{
    /**
     * Notifies the target class that implements this contract
     * @param sender A name-id of whoever is sending this
     * @param args  Miscellaneous arguments and parameters (it is up to the notification receiver to handle this)
     */
    onNotified(sender: any, args: Array<any>): void;
}
/**
 * Used to do library imports (reference CSS and JavaScript) in a single function. 
 * 
 * A few JavaScript libraries may not work properly 
 * due to the way your code initializes them.
 * At this time, you should resort to 
 * `<script />` import directly into 
 * the .HTML page.
 */
export class NativeLib
{
    libName: string;
    hasCss: boolean;
    hasJs: boolean;

    private cssPath: string;
    private jsPath: string;

    /**
     * Library to be imported.\
     * NOTE!!!: the root-path considered here is `'/lib/'` and this is determined by the static variable `PageShell.LIB_ROOT`
     * @param libName The library folder itself 
     * @param cssPath The name (or subpath) of the library's .css file. If not, ignore this parameter.
     * @param jsPath The name (or subpath) of the library's .js file. If not, ignore this parameter.
     */
    constructor({ libName = '', cssPath = '', jsPath = '' }: {
        libName?: string;
        cssPath?: string;
        jsPath?: string;
    })
    {
        this.libName = libName;
        this.cssPath = cssPath;
        this.jsPath = jsPath;
        this.hasCss = (cssPath != '' && cssPath != null);
        this.hasJs = (jsPath != '' && jsPath != null);
    }

    
    public getCssFullPath(): string
    {
        if(Misc.isNullOrEmpty(this.cssPath)) return '';
        return `${PageShell.LIB_ROOT}${this.libName}/${this.cssPath}`;
    }
    public getJsFullPath(): string
    {
        if (Misc.isNullOrEmpty(this.jsPath)) return '';
        return `${PageShell.LIB_ROOT}${this.libName}/${this.jsPath}`;
    }
    public toString(): string
    {
        return this.libName;
    }
}
/**
 * PageShell is a class that works at the lowest level (next to the page)
 * and performs some tasks in the DOM interface such as 
 * creating/finding/removing elements, 
 * directly importing native JS-CSS libraries 
 * and controlling access to resources such as SplitView, Storage and others.
 */
export class PageShell
{
    loadBSVersion()
    {
        if (PageShell.BOOTSTRAP_VERSION_NUMBER > 0) return;
        new VirtualFunction({
            fnName: 'getBSVersion',
            fnContent: `
                if(PageShell.BOOTSTRAP_VERSION == ''){
                    try
                    {
                        PageShell.BOOTSTRAP_VERSION = bootstrap.Tooltip.VERSION
                        PageShell.BOOTSTRAP_VERSION_NUMBER = parseFloat(bootstrap.Tooltip.VERSION)
                    }catch(e){
                        console.error('bootstrap.Tooltip.VERSION not found: ' + e.message)
                    }
                }`
        }).call()
    }

    /**defaults: '/lib/' */
    public static LIB_ROOT = '/lib/'

    private baseDocument: Document;
    public importedLibs: NativeLib[];
    private page: UIPage;

    private appStorageProvider: IAppStorageProvider = null;

    private appContainer: HTMLDivElement;
    private splitContainer: HTMLDivElement;

    public static BOOTSTRAP_VERSION = '';
    public static BOOTSTRAP_VERSION_NUMBER = 0.0;

    public static DISABLE_ANIMATION = false;

    constructor(mainDocument: Document, fsPage: UIPage) 
    {
        this.baseDocument = mainDocument;
        this.importedLibs = [];
        this.page = fsPage;


    }

    /**
     * Called from the `UIPage` implementation, 
     * enables the Storage feature, 
     * indicating a implementation of the `IAppStorageProvider` interface
     * @param provider 
     */
    public setStorageProvider(provider: IAppStorageProvider): void
    {
        this.appStorageProvider = provider;
    }

    /**
     * Called from the UIView or other high consumer-classes, requests an instance of `AppStorage` 
     * which must be resolved by the implementation of `IAppStorageProvider` 
     * (usually the same one that implements `UIPage`)
     * @param type `'local'` to LocalStorage or `'session'` to  SessionStorage
     * @param schemaName A unique name to demarcate a data context
     * @returns `AppStorage` instance
     */
    public requestStorage(type: string, schemaName: string): AppStorage
    {
        return this.appStorageProvider.onStorageRequested(type, schemaName);
    }

    /**
     * Enables the SplitView feature and allows two 
     * UIViews to be loaded simultaneously side-by-side on the page.
     * 
     * You must have marked this `<div id="app/split" />` previously in your HTML file.
     * 
     * @param appContainerId Id of the page's main app container div. The div that will display most UIView's in your app
     * @param splitContainerId Split container div id. The secondary UIView will be loaded in this Div
     */
    public enableSplitting(appContainerId: string, splitContainerId: string): void
    {
        this.appContainer = this.elementById(appContainerId) as HTMLDivElement;
        this.splitContainer = this.elementById(splitContainerId) as HTMLDivElement;

        if (this.splitContainer == null)
            throw new Error(`enable split fail: container Id '${splitContainerId}' not found in document. An div tag with this Id maybe not present.`);

        this.splitContainer.style.width = '0 px';
        this.splitContainer.hidden = true;
    }

    private currentViewSplitted: boolean = false;

    /**
     * Determines if SplitView is currently active
     */
    public isViewSplitted(): boolean
    {
        return this.currentViewSplitted;
    }

    /**
     * Sets the currently Splitted UIView to a reduced size
     * This will only work if `PageShell.isViewSplitted()` is `true`.
     */
    public shrinkSplitView()
    {
        if (this.currentViewSplitted == false) return;

        var self = this;
        this.splitContainer.hidden = false;
        var interv = setInterval(function ()
        {
            self.appContainer.classList.remove('col-6');
            self.appContainer.classList.add('col-9');

            self.splitContainer.classList.remove('col-6');
            self.splitContainer.classList.add('col-3');
            clearInterval(interv);
        });
        this.currentViewSplitted = true;

        self.splitContainer.style.borderLeft = '3px solid gray';
    }

    /**
     * Sets the currently Splitted UIView to a side-by-side size (50%)
     */
    public expandSplitView()
    {
        if (this.currentViewSplitted == false) return;

        var self = this;
        this.splitContainer.hidden = false;
        var interv = setInterval(function ()
        {
            self.appContainer.classList.remove(...self.appContainer.classList);
            self.appContainer.classList.add('col-6');

            self.splitContainer.classList.remove(...self.splitContainer.classList);
            self.splitContainer.classList.add('col-6');
            clearInterval(interv);
        });
        this.currentViewSplitted = true;

        self.splitContainer.style.borderLeft = '3px solid gray';
    }

    /**
     * Initializes a new UIView alongside the currently displayed UIView via SplitView features
     * @param ownerSplitView UIView currently displayed
     * @param splittedCallingView  New UIView that will be displayed next to the current one
     */
    public requestSplitView(splittedCallingView: UIView | UIFlatView | YordView): void
    {
        if (this.currentViewSplitted) return;

        var self = this;
        this.splitContainer.hidden = false;
        this.splitContainer.style.removeProperty('width')
        var interv = setInterval(function ()
        {
            self.appContainer.classList.remove(...self.appContainer.classList);
            self.appContainer.classList.add('col-9');
            clearInterval(interv);
        });
        this.currentViewSplitted = true;

        self.splitContainer.style.borderLeft = '3px solid gray';

        if (splittedCallingView instanceof UIFlatView) UIFlatView.load(splittedCallingView)
        else if (splittedCallingView instanceof UIView) this.navigateToView(splittedCallingView as unknown as UIView)
        else if (splittedCallingView instanceof YordView)
        {
            const ctx = new YordViewContext(this)
            ctx.addView(splittedCallingView)
            ctx.goTo((splittedCallingView as unknown as YordView).viewName)
        }
        else throw new Error(`requestSplitView(): Unsupported instance of 'splittedCallingView' parameter`)
    }

    /**
     * Fully collapse the SplitView Div and destroy the currently used UIView with Split
     */
    public closeSplitView()
    {
        if (this.currentViewSplitted == false) return;

        var self = this;
        this.splitContainer.innerHTML = '';
        this.splitContainer.hidden = true;
        var interv = setInterval(function ()
        {
            self.splitContainer.classList.remove(...self.splitContainer.classList);
            self.splitContainer.classList.add('col-3');

            self.appContainer.classList.remove(...self.appContainer.classList);

            self.appContainer.classList.add('col-12');
            clearInterval(interv);
        });
        this.currentViewSplitted = false;

    }

    /**
     * Creates (say "instance") a new HTMLElement object that represents an original HTML tag with its properties and attributes
     * @param tagName The exact name of the desired HTML5 tag
     * @param innerText (Optional) an initial text inserted as tag content (if the html element supports it)
     * @returns 
     */
    public createElement<K extends keyof HTMLElementTagNameMap>(tagName: K, innerText?: string)
    {
        var element = this.baseDocument.createElement(tagName);
        if (innerText != null)
            element.innerText = innerText;
        return element;
    }

    /**
     * Renders and brings to the front a view generated by a UIView object
     * @param view 
     */
    public navigateToView(view: UIView, preventClear: boolean = false): void
    {
        this.page.navigateToView(view, preventClear);
    }

    /**
     * Get the `<body>` of the page
     * @returns 
     */
    public getPageBody(): Element
    {
        return this.elementsByTagName('body')[0];
    }

    public elementsByTagName(tagName: string): HTMLCollectionOf<Element>
    {
        return this.baseDocument.getElementsByTagName(tagName);
    }

    public elementById(elementId: string): Element
    {
        return this.baseDocument.getElementById(elementId);
    }

    public appendChildToElement(containerElement: Element, childElement: Element): Element
    {
        return containerElement.appendChild(childElement);
    }

    public removeChildFromElement(containerElement: Element, childElement: Element): Element
    {
        return containerElement.removeChild(childElement);
    }

    public getImportedLib({ js: jsPath = null, css: cssPath = null }: {
        js?: string,
        css?: string
    }): NativeLib
    {
        if (this.importedLibs == undefined) return;
        for (var i = 0; i < this.importedLibs.length; i++)
        {
            if (!Misc.isNullOrEmpty(jsPath))
                if (this.importedLibs[i].getJsFullPath() == jsPath)
                    return this.importedLibs[i];

            if (!Misc.isNullOrEmpty(cssPath))
                if (this.importedLibs[i].getCssFullPath() == cssPath)
                    return this.importedLibs[i];
        }
        return null;
    }

    /**
     * Import a native JS-CSS library into the page, 
     * specifying the name and paths to the 
     * .js and .css content files
     */
    public import(lib: NativeLib): void
    {
        if (lib.libName != '')
        {
            var existing = this.getImportedLib({ js: lib.getJsFullPath(), css: lib.getCssFullPath() });
            if (existing !== null)
                return;
        }

        if (lib.hasCss)
        {
            var link: HTMLLinkElement = document.createElement('link');
            link.rel = 'stylesheet';
            link.type = 'text/css';
            link.href = lib.getCssFullPath();

            document.head.appendChild(link);
        }

        if (lib.hasJs)
        {
            var jsImport: HTMLScriptElement = document.createElement('script');
            jsImport.src = lib.getJsFullPath();
            jsImport.type = 'text/javascript'

            document.body.appendChild(jsImport);
        }

        this.importedLibs.push(lib);
    }

    public removeImport(libName: string)
    {
        const libs: NativeLib[] = this.getImportedLibByName(libName);
        for (var i = 0; i < libs.length; i++)
        {
            const lib = libs[i];
            if (lib.hasCss)
            {
                for (var c = 0; c < document.head.childNodes.length; c++)
                {
                    let child = document.head.childNodes[c];
                    if (child instanceof HTMLLinkElement)
                    {
                        let link = child as unknown as HTMLLinkElement;
                        if (link.href == lib.getCssFullPath())
                            link.remove();
                    }
                }
            }

            if (lib.hasJs)
            {
                for (var c = 0; c < document.body.childNodes.length; c++)
                {
                    let child = document.body.childNodes[c];
                    if (child instanceof HTMLScriptElement)
                    {
                        let scriptEl = child as unknown as HTMLScriptElement;
                        if (scriptEl.src == lib.getJsFullPath())
                            scriptEl.remove();
                    }
                }
            }
            this.importedLibs.splice(i, 1);
        }
    }

    public getImportedLibByName(libName: string): NativeLib[]
    {
        if (this.importedLibs == undefined) return [];
        let result: NativeLib[] = [];

        for (var i = 0; i < this.importedLibs.length; i++)
        {
            const imported = this.importedLibs[i];
            if (imported.libName == libName)
                result.push(imported);
        }
        return result;
    }
}
export class RowOptions
{
    rowHeidth?: string;
    rowClass?: string;

    columns?: Col[] = [];

    constructor()
    {
        this.rowClass = 'row';
    }
}


/**
 * Represents a Row Div with standard Bootstrap class options
 */
export class Row
{
    id: string;
    rowClass: string = 'row';
    rowWidth: string;
    rowHeidth: string;
    flexGrow1: boolean;
    rowColumns: Col[] = [];

    generatedColumnId: string;

    /**
     * 
     * @param id A div-container Id to parent this
     * @param options Row options like class, height and sub-columns; NOTE: if no column is provided, it may be that at least 
     * one column is generated automatically. To determine this, 
     * check the static variable `ViewLayout.AUTO_GENERATE_COLUMNS`
     */
    constructor(id: string, options: RowOptions)
    {
        this.id = id;

        if (Misc.isNullOrEmpty(options.rowClass) == false)
            this.rowClass = options.rowClass;
        if (Misc.isNullOrEmpty(options.rowHeidth) == false)
            this.rowHeidth = options.rowHeidth;
        if (Misc.isNull(options.columns) == false)
            this.rowColumns = options.columns;

        if ((this.rowColumns == null || this.rowColumns == undefined) || this.rowColumns.length == 0)
        {
            if (ViewLayout.AUTO_GENERATE_COLUMNS)
            {
                var id: string = `col_${Widget.generateUUID()}`;
                this.generatedColumnId = id;
                this.rowColumns = [
                    new Col(id, { colClass: 'col-md-12 col-xs-12 col-lg-12 col-sm-12' })
                ];
            }
        }
        else
        {
            for (var i = 0; i < this.rowColumns.length; i++)
            {
                var column: Col = this.rowColumns[i];
                if (Misc.isNullOrEmpty(column.colClass))
                    column.colClass = 'col-md-12 col-xs-12 col-lg-12 col-sm-12'
            }
        }
    }
}
export class Misc 
{
    public static isNull(value: any|object): boolean
    {
        return (value == null || value == undefined);
    }

    public static isNullOrEmpty(value: any|object): boolean
    {
        return (value == null || value == undefined || value == '');
    }

    /**
  * Public Domain/MIT
  * 
  * This function generates a UUID (universal unique identifier value) to bind to an HTML element
  */
    public static generateUUID()
    {
        var d = new Date().getTime();//Timestamp
        var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0;//Time in microseconds since page-load or 0 if unsupported
        var res = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c)
        {
            var r = Math.random() * 16;//random number between 0 and 16
            if (d > 0)
            {//Use timestamp until depleted
                r = (d + r) % 16 | 0;
                d = Math.floor(d / 16);
            } else
            {//Use microseconds since page-load if supported
                r = (d2 + r) % 16 | 0;
                d2 = Math.floor(d2 / 16);
            }
            var result = (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);

            return result;
        });

        return res.split('-')[0];
    }
}
/**
 * A common abstraction for local storage features, 
 * which can be persistent (aka 'LocalStorage') 
 * or temporary (aka 'SessionStorage')
 * */
export abstract class AppStorage
{
    public toString(): string
    {
        return 'APP-STORAGE (temp. and persist data management)';
    }

    protected type: string;
    protected schemaName: string;

    /**
     * To provide a concrete instance of this class, 
     * you must first implement `IAppStorageProvider` 
     * from your inherited `UIPage` class.
     * 
     * @param type 'local' or 'session'
     * @param schemaName A unique name to demarcate a data context
     */
    constructor(type: string, schemaName: string)
    {
        this.type = type;
        this.schemaName = schemaName;
    }

    /**
     * Writes a value to the Storage system. 
     * It is possible to send single data or entire objects
     * 
     * @param key A unique data identifier key
     * @param value The data itself. You can provide a simple value as a number or any text. But you can also send an entire object instance here.
     */
    public abstract write(key: string, value: any | object): void;

    /**
     * Updates a value in the storage system
     * 
     * @param key A unique data identifier key
     * @param value The data itself. You can provide a simple value as a number or any text. But you can also send an entire object instance here.
     */
    public abstract update(key: string, value: any | object): void;

    /**
     * Erases a value in the storage system
     * 
     * @param key A unique data identifier key
     */
    public abstract erase(key: string): void;

    /**
     * Retrieve a value in the storage system
     * 
     * @param key A unique data identifier key
     */
    public abstract get(key: string): any | object;
}
/**
 * Implementing this interface in an inherited `UIPage` 
 * class will enable the Storage feature in your application. 
 * 
 * The purpose of this interface is to provide an instance of the 
 * implementation of the `AppStorage` class
 */
export interface IAppStorageProvider
{
    /**
     * Implementing this interface will provide 
     * local and session storage capabilities 
     * for your app
     * 
     * It will occur when any UIView requests this feature.
     * You must return an implementation instance of `AppStorage`
     *
     * By default, you can use `RhabooStorageWrapper` implementation 
     * which is provided by Objective-UI library
     * 
     * Example: 
     * 
     * ```
    onStorageRequested(type: string, schemaName: string): AppStorage
    {
        return new RhabooStorageWrapper(type, schemaName);
    }
     * ```
     * 
     * @param type `'local'` to LocalStorage or `'session'` to  SessionStorage
     * @param schemaName A unique name to demarcate a data context
     */
    onStorageRequested(type: string, schemaName: string): AppStorage;
}
export class RhabooInstance
{
    public name: string = null;
    public instance: any | object = null;
}

export class RhabooStorageWrapper extends AppStorage
{
    public static INSTANCES: Array<RhabooInstance> = [];

    public static addInstance(instance: RhabooInstance): void
    {
        RhabooStorageWrapper.INSTANCES.push(instance);
    }

    private rhaboo: RhabooInstance;


    /**
     *  REQUIRED `<script src="lib/rhaboo/rhaboo.js"></script>`
     * 
     * 
     * This is an implemented portability of "Rhaboo" (https://github.com/adrianmay/rhaboo), 
     * a powerful JavaScript library that allows storing 
     * object graphs (with references) in 
     * LocalStorage or SessionStorage with great 
     * precision and consistency.
     */
    constructor(type: string, schemaName: string)
    {
        super(type, schemaName);

        var rhabooInstanceType = (type == 'local' ? 'persistent' : 'perishable');
        var activate = new VirtualFunction({
            fnName: 'rhabooInstance',
            fnContent: `
                var rb = Rhaboo.${rhabooInstanceType}('${schemaName}');
                RhabooStorageWrapper.addInstance({ name: '${schemaName}', instance: rb });
            `
        });
        activate.call();

        for (var i = 0; i < RhabooStorageWrapper.INSTANCES.length; i++)
        {
            var instance = RhabooStorageWrapper.INSTANCES[i];
            if (instance.name == schemaName)
            {
                this.rhaboo = instance;
                break;
            }
        }
    }

    public write(key: string, value: any): void
    {
        this.rhaboo.instance.write(key, value);
    }
    public update(key: string, value: any): void
    {
        this.erase(key);
        this.write(key, value);
    }
    public erase(key: string): void
    {
        this.rhaboo.instance.erase(key);
    }
    public get(key: string): any | object
    {
        return this.rhaboo.instance[key];
    }
}
export class SelectOption
{
    public value: any;
    public text: string;

    constructor(opValue: any, opText: string)
    {
        this.value = opValue;
        this.text = opText;
    }
}
export class ViewDictionaryEntry 
{
    originalId : string;
    managedId : string;

    constructor(originalId : string, managedId : string) {
        this.originalId = originalId;
        this.managedId = managedId;
    }

    getOriginalId() {
        return this.originalId;
    }

    getManagedId() {
        return this.managedId;
    }
}
/**
 * ViewLayout is a class that logically contains a demarcation 
 * of divs that will be used by the UIView inherited class, 
 * when this View is rendered. \
 * \
 * It is possible to build the layout in the form of an object:
 * ```
new ViewLayout('app', [
    new Row('row-X', { rowClass: 'row', rowHeidth: '100px', 
        columns: [
            new Col('col-Y-left', { colClass: 'col-8',  colHeight: '80px' }),
            new Col('col-Y-right', { colClass: 'col-4', colHeight: '20px' )
        ]
    }),
])
 * ``` 
 * attributes are optional but can take on unwanted default values.
 *
 * Or directly by a raw-html string:  
 * 
 * ```
new ViewLayout('app').fromHTML(`
    <div class="row-x" style="height:100px">
        <div id="col-Y-left"  class="col-8"  style="height:80px"> </div>
        <div id="col-Y-right" class="col-4" style="height:20px"> </div>
    </div>
`);
 * ```
 */
export class ViewLayout
{
    public static AUTO_GENERATE_COLUMNS = false;


    private layoutDOM: Document;
    public layoutRows: Row[];
    public containerDivObj: Element;
    private containerDivName: string;
    private rawHtml: string;
    private fromString: boolean = false;

    private layoutPresenter: ILayoutPresenter = new DefaultLayoutPresenter();

    /**
     * 
     * @param containerDivId Provide the 'Id' of the Div that will contain this layout (and consequently the Widgets elements)
     * @param rows Provide root rows for this layout. Ignore this parameter if you want to provide the layout from raw-html content (via `ViewLayout().fromHTML()`)
     */
    constructor(containerDivId: string, rows?: Row[])
    {
        this.layoutRows = rows;
        this.containerDivName = containerDivId;
    }

    public getRow(rowId: string): Row
    {
        if (this.fromString)
            throw new Error('getRow() is not supported when layout is output over raw html string');

        for (var i = 0; i < this.layoutRows.length; i++)
            if (this.layoutRows[i].id == rowId)
                return this.layoutRows[i];
        return null as unknown as Row;
    }

    /**
     * 
     * @param rawHtmlLayoutString Raw html snippet demarcating page layout with divs
You should avoid declaring user elements directly here.
     * @param staticData (Optional) An object to provide static values in the provided html-layout snippet.
You must use '#propertyName' in the raw-html to bind to the object given here. 
You can also concatenate directly to the raw-html string. Your choice 😍.

Example:
```
this.fromHTML(`
            <div class="row">
                <div class="col-5">
                    <h5> #supplierName </h5>
                </div>
            </div>
        `, { //data obj
            id: 1,
            supplierName: 'ACC/IO Systems'
        })
```
     * @returns 
     */
    fromHTML(rawHtmlLayoutString: string, staticData?: any | object): ViewLayout
    {
        if (!Misc.isNull(staticData))
        {
            for (var prop in staticData)
            {
                rawHtmlLayoutString = rawHtmlLayoutString.replace(`#${prop}`, staticData[prop])
            }
        }

        this.fromString = true;
        this.rawHtml = rawHtmlLayoutString;
        return this;
    }


    render(shellPage: PageShell, customPresenter?: ILayoutPresenter, preventClear: boolean = false): Element
    {
        this.containerDivObj = shellPage.elementById(this.containerDivName) as HTMLDivElement;

        if (this.fromString)
        {
            var parser = new DOMParser();
            var dom: Document = parser.parseFromString(this.rawHtml, 'text/html');
            this.layoutDOM = dom;

            if (preventClear == false)
                this.containerDivObj.innerHTML = '';
            var objDom = this.layoutDOM.children[0].children[1];

            if (UIPage.DEBUG_MODE)
            {
                const lb = document.createElement('label');
                lb.textContent = ` ViewLayout: #${this.containerDivObj.id}`;
                lb.style.color = 'green';
                objDom.append(lb)
            }

            for (var i = 0; i < objDom.childNodes.length; i++)
                this.containerDivObj.appendChild(objDom.childNodes[i]);

            return objDom;
        }

        if (undefined == shellPage || null == shellPage)
            throw 'PageShell instance is required here.';
        if (undefined != customPresenter || null != customPresenter)
            this.layoutPresenter = customPresenter;

        return this.layoutPresenter.renderLayout(this, shellPage);
    }

    ElementsIdCollection(): string[]
    {
        if (this.fromString)
        {
            var idCollection: Array<string> = [];
            var nodesWithId: NodeListOf<Element> = this.containerDivObj.querySelectorAll('*[id]');
            for (var i = 0; i < nodesWithId.length; i++)
                idCollection.push(nodesWithId[i].id);
            return idCollection;
        }
        return this.ScanRows(this.layoutRows);
    }

    private ScanRows(rows: Row[]): string[]
    {
        var result: string[] = [];
        if (rows !== undefined)
        {
            for (var i = 0; i < rows.length; i++)
            {
                var row: Row = rows[i];
                result.push(row.id);

                if (row.rowColumns !== undefined)
                {
                    var cols: string[] = this.ScanColumns(row.rowColumns);
                    for (var c = 0; c < cols.length; c++)
                        result.push(cols[c]);
                }
            }
        }
        return result;
    }

    private ScanColumns(columns: Col[]): string[]
    {
        var result: string[] = [];
        for (var i = 0; i < columns.length; i++)
        {
            var col = columns[i];
            result.push(col.id);

            if (col.columnRows !== null)
            {
                var rows: string[] = this.ScanRows(col.columnRows);
                for (var r = 0; r < rows.length; r++)
                    result.push(rows[r]);
            }
        }
        return result;
    }

    /**
   * 
   * @param layoutUri /path/to/layout.html
   * @param intoCallBack callback to receive raw-html string
   * ```
   * function(html: string) {  }
   * ``` 
   */
    public static load(layoutUri: string, intoCallBack: Function)
    {
        if (!layoutUri.endsWith('.html'))
            layoutUri += '.html';

        fetch(layoutUri)
            .then(function (r)
            {
                return r.text();
            })
            .then(function (r)
            {
                intoCallBack(r);
            });
    }
}
/**
 * The WidgetFragment has the ability to "draw" Widget objects 
 * into a LayoutId (html div) of the page. It is under the control 
 * of a WidgetContext and can manage several child Widgets.
 * 
 * It is also capable of attaching and detaching Widgets,
 * brokering message requests sent by child-Widgets 
 * and submitting them to its own-WidgetContext.
 * 
 */
export class WidgetFragment implements INotifiable
{

    contextRoot: WidgetContext;
    fragmentId: string;
    containerElement: HTMLDivElement;
    widgets: Widget[];
    widgetsLoaded: number;

    /**
     * 
     * @param appContextRoot The parent WidgetContext
     * @param containerElement An (Element object) HTML element to compose the adjacent Widgets. Usually Div's.
     */
    constructor(appContextRoot: WidgetContext, containerElement: HTMLDivElement)
    {
        this.contextRoot = appContextRoot;
        this.fragmentId = containerElement.getAttribute('id');

        //div object (not id)
        this.containerElement = containerElement;
        this.widgets = [];
    }

    clear()
    {
        this.containerElement.innerHTML = '';
        for (var i = 0; i < this.widgets.length; i++)
        {
            try
            {
                this.widgets[i].onWidgetDetached();
                this.containerElement.removeChild(this.widgets[i].getDOMElement());
            } catch { }
        }
        this.containerElement.innerHTML = '';
        this.widgets = [];
    }

    /**
     * Submit a message request sent by the Widget directed to the parent-Context
     * @param widgetName The widget instance-name
     * @param messageId An "ID" of the message. The Widget may have its own message ID catalog for its respective handling cases.
     * @param messageText A text for the message
     * @param messageAnyObject Any object defined by the Widget itself. There are no restrictions on the object type.
     */
    pushMessageToRoot(
        widgetName: string,
        messageId: number,
        messageText: string,
        messageAnyObject: object): void
    {

        this.contextRoot.pushMessage(widgetName,
            messageId,
            messageText,
            messageAnyObject);
    }

    /**
     * Gets an Widget object instance
     * @param name Widget Instance Name
     * @returns Widget
     */
    findWidget(name: string): Widget
    {
        for (var i = 0; i < this.widgets.length; i++)
        {
            var widget: Widget = this.widgets[i];
            if (widget.widgetName == name)
                return widget;
        }

        return null;
    }

    /**
     * (Internal) method responsible for controlling the 
     * Widgets loading stack, and immediately notifying 
     * the parent-Context when the stack is
     * terminated.
     */
    private onWidgetLoad()
    {
        this.widgetsLoaded += 1;
        if (this.widgetsLoaded == this.widgets.length) //stack is end
            this.contextRoot.onFragmentLoad(); //notify to parent-ctx: "all Widgets been loaded :)""
    }

    resetFragment(): void
    {
        this.containerElement.innerHTML = '';
    }

    /**
     * Renders the Child Widgets stack on the specified Container
     */
    renderFragmentwidgets()
    {
        this.widgetsLoaded = 0;

        if (this.widgets.length == 0)
            this.contextRoot.onFragmentLoad();
        else
        {
            var self = this;
            var shell: PageShell = this.contextRoot.contextShell();

            if (PageShell.DISABLE_ANIMATION == false)
                self.containerElement.style.opacity = '0';

            for (var i = 0; i < self.widgets.length; i++)
            {
                var widget = self.widgets[i];
                widget.renderView(this as INotifiable);
            }

            if (UIPage.DEBUG_MODE)
            {
                const lb = document.createElement('label');
                lb.textContent = `Fragment: #${self.containerElement.id}`;
                lb.style.color = 'blue';
                self.containerElement.append(lb);
                self.containerElement.append(document.createElement('br'));
            }

            if (PageShell.DISABLE_ANIMATION == false)
            {
                var opacity = 0;
                var interv = setInterval(function ()
                {
                    if (opacity < 1)
                    {
                        opacity = opacity + 0.070
                        self.containerElement.style.opacity = opacity.toString();

                    }
                    else clearInterval(interv);
                });
            }
        }
    }

    onNotified(sender: any, args: any[]): void
    {
        if (sender == 'FSWidget')
            this.onWidgetLoad();
    }

    /**
     * Attach a Widget to the Fragment
     * @param widget An Widget object
     */
    attatchWidget(widget: Widget)
    {
        for (var i = 0; i < this.widgets.length; i++)
        {
            var existingWidget: Widget = this.widgets[i];
            if (widget.widgetName == existingWidget.widgetName)
                throw `widget '${widget.widgetName}' has already been attached to this context.`;
        }

        widget.setParentFragment(this);
        this.widgets.push(widget);
    }

    /**
     * Detach a Widget from the Fragment
     * @param widget An Widget object 
     */
    dettatchwidget(widget: Widget): void
    {
        var toRemove = -1;
        for (let index = 0; index < this.widgets.length; index++)
        {
            var existingWidget: Widget = this.widgets[index];
            if (existingWidget.widgetName == widget.widgetName)
            {
                toRemove = index;
                break;
            }
        }

        if (toRemove > -1)
        {
            this.widgets.splice(toRemove, 1);
            this.containerElement.removeChild(widget.getDOMElement());
            widget.onWidgetDetached();
        }
    }

    appendChildElementToContainer(elementChild: Element)
    {
        this.contextRoot.contextShell().appendChildToElement(
            this.containerElement,
            elementChild
        );
    }
}
export class WidgetMessage
{
    widgetName: string;
    messageId: number;
    messageText: string;
    messageAnyObject: object;

    constructor(widgetName: string, 
        messageId: number, 
        messageText: string, 
        messageAnyObject: object)
    {
        this.widgetName = widgetName;
        this.messageId = messageId;
        this.messageText = messageText;
        this.messageAnyObject = messageAnyObject;
    }
}
/**
 * Represents a native JavaScript function virtually controlled by TypeScript
 * 
 * Through this class, a JavaScript function will be 
 * dynamically placed in the DOM of the page, 
 * executed and then destroyed. 
 * 
 * From this, it is possible to invoke functions from 
 * native JavaScript libraries from the written TypeScrit 
 * code.
 */
export class VirtualFunction
{
    public functionName: string;
    public functionArgs: string[];
    public functionBodyContent: string;
    private keep: boolean;
    public functionId: string;

    public toString(): string
    {
        return `function ${this.functionName}(${this.argNamesStr()});`;
    }

    /**
     * Defines a JavaScript virtual function
     * @param fnName the function name
     * @param fnArgNames An array with the names of the function's arguments (the variables that the function takes)
     * @param fnContent The literal body of the function; NOTE: you must not specify `{ or } ` here. Only the raw body of the function is allowed
     * @param keepAfterCalled Determines whether the function should remain active on the page after the first call. By default it is false.
     */
    constructor({ fnName, fnArgNames = [], fnContent, keepAfterCalled = false }:
        {
            fnName: string,
            fnArgNames?: string[],
            fnContent?: string,
            keepAfterCalled?: boolean,
        })
    {
        this.functionName = fnName;
        this.keep = keepAfterCalled;
        this.functionArgs = fnArgNames;
        this.functionBodyContent = fnContent;
        this.functionId = Misc.generateUUID();
    }

    /**
     * @param fnContent The literal body of the function; NOTE: you must not specify `{ or } ` here. Only the raw body of the function is allowed
     */
    setContent(fnContent: string): VirtualFunction
    {
        this.functionBodyContent = fnContent;
        return this;
    }

    /**
     * Calls the JavaScript function. 
     * Here the function will materialize in the 
     * DOM as a `<script> function here </script>` tag and the 
     * function will be inside it
     * @param argValues An array with the VALUES of the arguments defined in the function. Note that you must pass the array according to the actual parameters of the function.
     */
    call(...argValues: string[]): VirtualFunction
    {
        var argNamesStr = this.argNamesStr();
        var argValuesStr = this.argValuesStr(...argValues);

        var fnString = `function ${this.functionName}(${argNamesStr}) {
            ${this.functionBodyContent}
        }
        
        ${this.functionName}(${argValuesStr});`;

        var fn = document.createElement('script');
        fn.id = this.functionId;
        fn.textContent = fnString;

        var els = document.getElementsByTagName('body');
        els[0].append(fn);

        if (this.keep == false)
            fn.remove();

        return this;
    }

    private argValuesStr(...argValues: string[]): string
    {
        var argValuesStr = '';
        for (var a = 0; a < argValues.length; a++)
            argValuesStr += `'${argValues[a]}', `;
        if (argValuesStr.endsWith(', '))
            argValuesStr = argValuesStr.substring(0, argValuesStr.length - 2);
        return argValuesStr;
    }

    private argNamesStr(): string
    {
        var argNamesStr = '';
        for (var a = 0; a < this.functionArgs.length; a++)
            argNamesStr += `${this.functionArgs[a]}, `;
        if (argNamesStr.endsWith(', '))
            argNamesStr = argNamesStr.substring(0, argNamesStr.length - 2);
        return argNamesStr;
    }
}
/**
 * An wrapper of jQuery lib ported for Objective-UI
 */
export class jQuery
{
    private static callback: Function = null;

    /**
     * 
     * @param idOrClass 
     * @param fn 
     * ``` 
     *  function($:any) { 
        // $ is jQuery instance
      } 
     * ```
     */
    public static $(idOrClass: string, fn: Function)
    {
        jQuery.callback = fn;
        new VirtualFunction({
            fnName: 'wrapQuery',
            fnArgNames: ['idOrClass'],
            fnContent: `
                jQuery.callback($(idOrClass));
                jQuery.callback = null;
            `    ,
            keepAfterCalled: true
        },).call(idOrClass);
    }
}
/**
 * A class that generates a simplified, 
 * standard Exception view at the point on 
 * the page where an error occurred
 */
export class DefaultExceptionPage
{
    constructor(error: Error)
    {
        if ((error instanceof Error) == false) return;
        console.error(error);
        if (UIPage.DISABLE_EXCEPTION_PAGE)
            return;

        var errorsStr = `${error.stack}`.split('\n');
        var title = `${error}`;
        var paneId = Widget.generateUUID();
        var rawHtml = `<div id="exceptionPane_${paneId}">`;
        rawHtml += `<img style="padding-left:30px; padding-top: 80px" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAHYAAAB2AH6XKZyAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAABFZJREFUeJztm0GLFEcUx/+vpzurQhaVCKJB/QSehJwjgrCzB4egm4PsIcJOEslNkNw8GQQ9Rp0h5GhcJQ4kuKeQfAEvCehRHNwNaxDUJbqZme56Ocz2Tk93VXXPdndZ7PaD3enueq/q/V6/elU9vQtUUkkllVSyc4VUDVfX/FlitFjgEBgIf1hEjll2zGM6Sj0xfh08/FDZAZvnK4KpefUT92ERAXBUDZbCA4zDDvPtIuC1AbAUPmz/uPQAWAy/2WYkAFbCmwrAdocH9DXAWng2OgVshDc2BWyFNxEAq+HfzxSwB95YDbAW3uwU2L7wQOoUsBTezBSwF95IDbAa3kwGyJ20At5kALYzPLCVnaAN8CYywGb4IpMg+xSwCd5EBuwEeCDLFLAR3lQGbHd4AHCVLebg/2OgVRN8p+d7j6+fprfFIupFGQBD8MsUiPp3p6b+Kh4tm6gzwMCdF8yz194jPJBlGYwcFznnGWhdO/nBnyUwTSTaDCiz4NUE34kO9+1v/iwLbjE23khhpJsshKxpi10XtAKm5vXP5O8S1ctgifBgwHO8J2PjlQE//HUYpH6XOPHTYBHwzMCVT+nf2HDFw4fXwMp3iRM9DRYFP3IsNp4KJOx4S/B6yfw0GPpQCLzKuYiuAH4PhHuMAvcohFhKh+elAP4RDoJjzPhD2q9Esu0DDMMzACHcL27UqQsAC4/4zL5Vf5GZG3J46rx54c21mzQAgEu/rF+AX3uaBg9kqQFlwGuCEAK6YqTVPkGDVwfdOQZ15PDuJjwAuMFGa4ZpoA+ADi4HvNSvWGoz9W8tPGIvbG6foMGbv905cDQISfiFFns+nO/z14Ay4SXOjac2A6CZvav9zjdLPBXqtJs0eNZzzxJwlwU9kMFPH/AXIWgm2fmEATD+PB+FH93h+q7A/2mhNcqE++coeL3qza/9434uhWduZIUHit4HQK6vrNyJMVliw429h5KZUAQ8UOQ+AHL9fPAbp4Lquwf9n6NBCCUPPFDUPkChr4SXOZdiI4TT6z2HiJu92gcB5p60r/A0dw2In+eEV/qjsGFBD+JzPpT75yjoBt55AHfHxolyaCTfPgDYGrwiA+Q21JEVvHhhXHvpzUNQZ6zL3DUAhuAht9EtddMfJQtjl92zoGEmZIEHtroPAPLBy7yL2RDzQ/1SR/Wp9eQSufbSm2fmzH9HPPk+AMgPr7o7ERsfwVfpSx03pvf7i9EgtJs0gCMuSvuVyGT7AMAIfFxFu9RRMgjSfhWS+qVoKfCaIIQ2DtwfL99bv+DXQOT0b2q3t8SND/f3O5fvrX/t10AIaj+ANONERP+dYEnwUr+iNkOFU4Fb6xKAxOof62BYUqjuU60LAWSFB7LUgNC5IuFVNZAlbSnn0mqf1kdEdDVgJTQuA/7Kr7xnbDxBKyXCP5doAtD+xwg1wVgu5c4z8I4Hx8cdpSZAyykgo9PM8LTMcL6UaFdSSSWVVLLj5X+IDiuFkg1oQQAAAABJRU5ErkJggg==" />`;
        rawHtml += `<h4 style="padding-left:30px"> ${title} </h4>`;
        rawHtml += `<p style="padding-left:30px; font-size: 20px">`;
        for (var i = 0; i < errorsStr.length; i++)
        {
            var erro = errorsStr[i];
            var msg = erro.trim();

            if (msg.indexOf('at') == 0)
            {
                var codePath = msg.substring(3, msg.indexOf('('));
                codePath = codePath.trim();
                msg = msg.replace(codePath,
                    `<span class="badge badge-secondary"> ${codePath} </span>`);
            }

            rawHtml += `${msg} <br/>`;
        }

        rawHtml += `</p>
            <button type="button" onclick="document.getElementById('exceptionPane_${paneId}').remove()" style="margin-left:30px; margin-bottom: 30px" class="btn btn-warning"> Hide </button>
        </div>
        `;

        var c = new DOMParser().parseFromString(rawHtml, 'text/html').body;
        document.body.prepend(c);
    }
}
export class UIHeadBinder extends WidgetBinder
{
    public head: UIHead;
    constructor(head: UIHead)
    {
        super(head);
        this.head = head;
    }

    getWidgetValue()
    {
        return this.head.value();
    }
    refreshUI(): void
    {
        var propValue = this.getModelPropertyValue();
        this.head.setText(`${propValue}`);
    }
    fillPropertyModel(): void { }
}

export class UIHead extends Widget implements IBindable
{
    private headType: string;
    private textContent: string;
    public headElement: HTMLHeadElement;
    private cssClass: string;
    constructor({ name, headType, text, cssClass }:
        {
            name: string,
            headType: string,
            text: string,
            cssClass?: string
        })
    {
        super(name);

        if (headType == '' || headType == null || headType == undefined)
            headType = 'H1';

        this.textContent = text;
        this.headType = headType
            .replace('<', '')
            .replace('/', '')
            .replace('>', '');
        this.cssClass = cssClass;
    }
    getBinder(): WidgetBinder
    {
        return new UIHeadBinder(this);
    }
    protected htmlTemplate(): string
    {
        return `<${this.headType} id="fsHead"> </${this.headType}>`
    }
    protected onWidgetDidLoad(): void
    {
        this.headElement = this.elementById('fsHead');
        this.headElement.textContent = this.textContent;

        if (!Misc.isNullOrEmpty(this.cssClass))
        {
            var classes = this.cssClass.split(' ');
            for (var c = 0; c < classes.length; c++)
                this.addCSSClass(classes[c])
        }
    }

    public setCustomPresenter(presenter: ICustomWidgetPresenter<Widget>): void
    {
        presenter.render(this);
    }

    public setText(text: string): void
    {
        this.headElement.textContent = text;
    }

    public value(): string
    {
        return this.headElement.textContent;
    }
    public setEnabled(enabled: boolean): void
    {
        throw new Error("Method not implemented.");
    }
    public addCSSClass(className: string): void
    {
        this.headElement.classList.add(className);
    }
    public removeCSSClass(className: string): void
    {
        this.headElement.classList.remove(className);
    }
    public applyCSS(propertyName: string, propertyValue: string): void
    {
        this.headElement.style.setProperty(propertyName, propertyValue);
    }
    public setPosition(position: string, marginLeft: string, marginTop: string, marginRight: string, marginBottom: string, transform?: string): void
    {
        this.headElement.style.position = position;
        this.headElement.style.left = marginLeft;
        this.headElement.style.top = marginTop;
        this.headElement.style.right = `${marginRight}`;
        this.headElement.style.bottom = `${marginBottom}`;
        this.headElement.style.transform = `${transform}`;
    }
    public setVisible(visible: boolean): void
    {
        this.headElement.style.visibility = (visible ? 'visible' : 'hidden')
    }

}
export class ModalAction
{
    public text: string;
    public classes: string[] = [];
    public onClick?: Function;
    public dismis: boolean;

    constructor({ buttonText, dataDismiss = false, buttonClasses = 'btn btn-light', buttonClick = null }:
        {
            buttonText: string;
            dataDismiss?: boolean;
            buttonClick?: Function;
            buttonClasses?: string;
        })
    {
        this.text = buttonText;

        this.onClick = buttonClick;
        this.dismis = dataDismiss;

        const classesStr = buttonClasses.split(' ');
        for (var c = 0; c < classesStr.length; c++)
            this.classes.push(classesStr[c]);

        if (this.text == null)
            this.text = 'Modal action';
        if (this.classes == null || this.classes.length == 0)
            this.classes = ['btn', 'btn-primary'];
    }

    public setButton(button: HTMLButtonElement, modal: UIDialog)
    {
        var self = this;
        if (Misc.isNull(this.onClick) == false)
            button.onclick = function ()
            {
                self.onClick(modal);
            };
    }
}
export class UIRadioGroupBinder extends WidgetBinder
{
    private radioGp: UIRadioGroup;
    constructor(radioGroup: UIRadioGroup)
    {
        super(radioGroup);
        this.radioGp = radioGroup;
    }

    getWidgetValue()
    {
        return this.radioGp.value();
    }
    refreshUI(): void
    {
        var value = this.getModelPropertyValue();
        this.radioGp.setValue(value);
    }
    fillPropertyModel(): void
    {
        var value = this.getWidgetValue();
        this.setModelPropertyValue(value);
    }

}

export class UIRadioGroup extends Widget implements IBindable
{
    public groupContainer: HTMLDivElement;
    public groupTitle: HTMLLabelElement;
    public fieldSet: HTMLFieldSetElement;


    private options: Array<UIRadioOption> = [];
    private title: string;
    private orientation: string;

    private initialOptions: Array<any> = [];
    private onChangeFn: Function;

    private containerClass: string;

    /**
    * @param onChange  ``` 
    * (selected: UIRadioOption, group: UIRadioGroup) => { } 
    * ```
    * @param direction Flex direction: 'column' / 'row'
    * @param options array { t:'Option Text', v: 'option_value' }
    */
    constructor({ name, title = '', containerClass = '', orientation = 'vertical', options = [], onChange = null }:
        {
            name: string,
            containerClass?: string,
            title?: string,
            orientation?: string,
            options?: Array<any>,
            onChange?: Function
        })
    {
        super(name);

        this.containerClass = containerClass;
        this.title = title;
        this.orientation = orientation;
        this.initialOptions = options;
        this.onChangeFn = onChange;
    }
    getBinder(): WidgetBinder
    {
        return new UIRadioGroupBinder(this);
    }

    public optionChanged(currentOp: UIRadioOption)
    {
        if (Misc.isNull(this.onChangeFn) == false)
            this.onChangeFn(currentOp, this);
    }

    public setOnChangedEvent(fn: Function)
    {
        this.onChangeFn = fn;
    }

    protected onWidgetDidLoad(): void
    {
        this.groupContainer = this.elementById('fsRadioGroup');
        this.groupTitle = this.elementById('groupTitle');
        this.fieldSet = this.elementById('fieldSet');

        this.groupTitle.textContent = this.title;

        if (this.orientation != 'horizontal' && this.orientation != 'vertical')
            throw new Error(`Invalid value '${this.orientation}' for 'orientation' parameter. Accepted values are 'vertical' or 'horizontal'`);

        if (this.orientation == 'vertical')
            this.fieldSet.classList.add(`flex-column`);
        if (this.orientation == 'horizontal')
            this.fieldSet.classList.add(`flex-row`);

        this.addOptions(this.initialOptions);
    }

    protected htmlTemplate(): string
    {
        return `
<div id="fsRadioGroup" class="${this.containerClass}">
  <label id="groupTitle" class="font-weight-normal" style="margin-left: 3px"> </label>
  <fieldset class="d-flex" id="fieldSet">

  </fieldset>
</div>`;
    }

    /**
     * 
     * @param options  array { t:'Option Text', v: 'option_value' }
     */
    addOptions(options: Array<any>)
    {
        for (var i = 0; i < options.length; i++)
        {
            var op = options[i];
            this.addOption(op.t, op.v);
        }
    }

    addOption(text: string, value: string)
    {
        var newOpt: UIRadioOption = new UIRadioOption(
            text,
            value,
            this.fieldSet.id,
            this.getPageShell(),
            this
        );
        this.options.push(newOpt);
        this.fieldSet.appendChild(newOpt.optionContainer);
    }

    addOptionR(newOpt: UIRadioOption)
    {
        this.options.push(newOpt);
        this.fieldSet.appendChild(newOpt.optionContainer);
    }

    fromList(models: Array<any>, textKey: string, valueKey: string)
    {
        for (var i = 0; i < models.length; i++)
        {
            var model = models[i];
            var text = model[textKey];
            var value = model[valueKey];
            this.addOption(text, value);
        }
    }

    public setCustomPresenter(renderer: ICustomWidgetPresenter<Widget>): void
    {
        renderer.render(this);
    }

    public selectedOption(): UIRadioOption
    {
        for (var i = 0; i < this.options.length; i++)
            if (this.options[i].isChecked())
                return this.options[i];
    }

    public setValue(value: string): void
    {
        for (var i = 0; i < this.options.length; i++)
        {
            if (this.options[i].value() == `${value}`)
                this.options[i].setChecked(true);
            else
                this.options[i].setChecked(false);
        }

        if (!Misc.isNull(this.onChangeFn))
            this.onChangeFn(this.selectedOption(), this);
    }

    public value(): string
    {
        for (var i = 0; i < this.options.length; i++)
        {
            var op = this.options[i];
            if (op.isChecked())
                return op.value();
        }
        return '';
    }
    public setEnabled(enabled: boolean): void
    {
        for (var i = 0; i < this.options.length; i++)
        {
            var op = this.options[i];
            op.setEnabled(enabled);
        }
    }

    public setPosition(position: string, marginLeft: string, marginTop: string, marginRight: string, marginBottom: string, transform?: string): void
    {
        this.groupContainer.style.position = position;
        this.groupContainer.style.left = marginLeft;
        this.groupContainer.style.top = marginTop;
        this.groupContainer.style.right = marginRight;
        this.groupContainer.style.bottom = marginBottom;
        this.groupContainer.style.transform = transform;
    }
    public setVisible(visible: boolean): void
    {
        this.groupContainer.style.visibility = (visible ? 'visible' : 'hidden')
    }

}
export class UIRadioOption
{
    public optionContainer: HTMLDivElement;
    public radioInput: HTMLInputElement;
    public radioLabel: HTMLLabelElement;
    private ownerGroup: UIRadioGroup;
    public text: string;

    constructor(text: string,
        value: string,
        fieldSetId: string,
        shell: PageShell,
        ownerGroup: UIRadioGroup,
        customTemplate?: string)
    {

        if (!Misc.isNullOrEmpty(customTemplate))
        {
            if (customTemplate.indexOf('radioOptionContainer') == -1)
                throw new Error(`RadioOption '${text} / ${value}' failed to load: custom base-template does not contains an <div/> with Id="radioOptionContainer".`)
            if (customTemplate.indexOf('radioInput') == -1)
                throw new Error(`RadioOption '${text} / ${value}' failed to load: custom base-template does not contains an <input/> with Id="radioInput".`)
            if (customTemplate.indexOf('radioLabel') == -1)
                throw new Error(`RadioOption '${text} / ${value}' failed to load: custom base-template does not contains an <label/> with Id="radioLabel".`)
        }

        var defaultTpl = (PageShell.BOOTSTRAP_VERSION_NUMBER < 5
            ? `
            <div id="radioOptionContainer" style="margin-right: 10px" class="custom-control custom-radio">
                <input id="radioInput" type="radio" name="fieldset" class="custom-control-input">
                <label id="radioLabel" class="custom-control-label font-weight-normal" for=""> Radio Option </label>
            </div>`
            : `
            <div id="radioOptionContainer" class="form-check me-3 mb-2 mt-2">
                <input id="radioInput" class="form-check-input" type="radio" name="flexRadioDefault" >
                <label id="radioLabel" class="form-check-label" for="">
                    Default radio
                </label>
            </div>
            `)

        var template: UITemplateView = new UITemplateView(
            Misc.isNullOrEmpty(customTemplate)
                ? defaultTpl
                : customTemplate,
            shell);

        this.ownerGroup = ownerGroup;
        this.optionContainer = template.elementById('radioOptionContainer');
        this.radioInput = template.elementById('radioInput');
        this.radioLabel = template.elementById('radioLabel');

        this.radioLabel.textContent = text;
        this.radioInput.value = value;
        this.radioInput.name = fieldSetId;
        this.radioLabel.htmlFor = this.radioInput.id;
        this.text = text;

        var self = this;
        this.radioInput.onclick = function (ev: Event)
        {
            ownerGroup.optionChanged(self);
        }
    }
    isChecked(): boolean
    {
        return this.radioInput.checked;
    }

    value(): string
    {
        return this.radioInput.value;
    }

    setChecked(isChecked: boolean): void
    {
        if (isChecked)
            this.ownerGroup.optionChanged(this);
        this.radioInput.checked = isChecked;
    }

    setEnabled(isEnabled: boolean): void
    {
        this.radioInput.disabled = (isEnabled == false);
    }
}

/**
 * Exclusive Bootstrap 5.x UIRadioOption compat.
 */
export class UIRadioOptionBS5 extends UIRadioOption
{
    constructor(text: string,
        value: string,
        fieldSetId: string,
        shell: PageShell,
        ownerGroup: UIRadioGroup)
    {
        super(text, value, fieldSetId, shell, ownerGroup,
            `
<div id="radioOptionContainer" style="margin-right: 10px" class="form-check form-check-primary mt-3">
  <input id="radioInput" name="default-radio-1" class="form-check-input" type="radio" value="" id="defaultRadio2">
  <label id="radioLabel" class="form-check-label" for="defaultRadio2">
      Checked
  </label>
</div> `)
    }

}

export class UIButton extends Widget
{
    public buttonElement: HTMLButtonElement;
    public imageElement: HTMLImageElement;

    public text: string;
    public onClick: Function;
    public btnClass: string;
    public imageSrc: string;
    public imageWidth: string;

    constructor({ name, text, imageSrc, imageWidth = '20px', btnClass = 'btn' }:
        {
            name: string;
            text: string;
            imageSrc?: string;
            imageWidth?: string,
            btnClass?: string
        })
    {
        super(name);

        this.imageSrc = imageSrc;
        this.imageWidth = imageWidth;
        this.text = text;
        this.btnClass = btnClass;
    }

    protected htmlTemplate(): string
    {
        if (this.imageSrc != '' && this.imageSrc != null && this.imageSrc != undefined)
        {
            return `
<button id="fsButton" type="button" class="btn">
     <img alt="img" id="fsButtonImage" src="${this.imageSrc}" style="width: ${this.imageWidth}"></img> 
</button>`
        }
        else
            return `<button id="fsButton" type="button" class="btn"> Button </button>`
    }
    protected onWidgetDidLoad(): void
    {
        var self = this;
        this.buttonElement = this.elementById('fsButton');
        this.imageElement = this.elementById('fsButtonImage');
        this.setText(this.text);

        if (Misc.isNullOrEmpty(this.btnClass) == false)
        {
            var btnClasses: Array<string> = this.btnClass.split(' ');

            for (var i = 0; i < btnClasses.length; i++)
            {
                var className = btnClasses[i].trim();
                if (!this.buttonElement.classList.contains(className))
                    this.buttonElement.classList.add(className);
            }
        }

        if (self.onClick != null)
        {
            this.buttonElement.onclick = function (ev)
            {
                self.onClick(ev);
            };
        }
    }

    public setOnClickFn(clickFn: Function)
    {
        const self = this;
        this.onClick = clickFn;
        if (!Misc.isNull(this.buttonElement))
            this.buttonElement.onclick = function ()
            {
                self.onClick()
            };
    }

    public setText(text: string)
    {
        this.buttonElement.innerText = text;

        if (this.imageSrc != '' && this.imageSrc != null && this.imageSrc != undefined)
        {
            this.imageElement.src = this.imageSrc;
            this.imageElement.style.width = this.imageWidth;
            this.buttonElement.appendChild(this.imageElement);
        }
    }

    public value(): string
    {
        throw new Error("Button does not support value");
    }

    public setVisible(visible: boolean): void
    {
        this.buttonElement.style.visibility = (visible ? 'visible' : 'hidden')
    }

    public setEnabled(enabled: boolean): void
    {
        this.buttonElement.disabled = (enabled == false);
    }

    public addCSSClass(className: string): void
    {
        this.buttonElement.classList.add(className);
    }

    public removeCSSClass(className: string): void
    {
        this.buttonElement.classList.remove(className);
    }

    public applyCSS(propertyName: string, propertyValue: string): void
    {
        this.buttonElement.style.setProperty(propertyName, propertyValue);
    }

    public setPosition(position: string,
        marginLeft: string,
        marginTop: string,
        marginRight?: string,
        marginBottom?: string,
        transform?: string): void
    {
        this.buttonElement.style.position = position;
        this.buttonElement.style.left = marginLeft;
        this.buttonElement.style.top = marginTop;
        this.buttonElement.style.right = `${marginRight}`;
        this.buttonElement.style.bottom = `${marginBottom}`;
        this.buttonElement.style.transform = `${transform}`;
    }

    public setCustomPresenter(renderer: ICustomWidgetPresenter<UIButton>): void
    {
        renderer.render(this);
    }
}
export class UICheckBoxBinder extends WidgetBinder
{
    private checkBox: UICheckBox;
    constructor(checkBox: UICheckBox)
    {
        super(checkBox);
        this.checkBox = checkBox;
    }

    refreshUI(): void
    {
        var value = this.getModelPropertyValue();
        this.checkBox.setChecked(value);
    }
    fillPropertyModel(): void
    {
        var checked: boolean = this.checkBox.isChecked();
        this.setModelPropertyValue(checked);
    }
    getWidgetValue()
    {
        var checked: boolean = this.checkBox.isChecked();
        return checked;
    }
}

export class UICheckBox extends Widget implements IBindable
{
    public divContainer: HTMLDivElement;
    public checkElement: HTMLInputElement;
    public checkLabel: HTMLLabelElement;
    public onCheckedChange: Function;

    private initialChecked: boolean;
    private labelText: string;
    private customBaseTemplate: string;

    /**
     * 
     * @param fnOnChange 
     * ```
     * (checked: boolean, checkBox: UICheckBox) => {  }
     * ```
     */
    constructor({ name, text, checked = false, customTemplate = null, onCheckedChanceFn = null }:
        {
            name: string;
            text: string;
            checked?: boolean;
            customTemplate?: string,
            onCheckedChanceFn?: Function
        })
    {
        super(name);

        this.labelText = text;
        this.initialChecked = checked;
        this.customBaseTemplate = customTemplate;
        this.onCheckedChange = onCheckedChanceFn;
    }
    getBinder(): WidgetBinder
    {
        return new UICheckBoxBinder(this);
    }

    protected htmlTemplate(): string
    {
        if (!Misc.isNullOrEmpty(this.customBaseTemplate))
        {
            if (this.customBaseTemplate.indexOf('UICheckBox') == -1)
                throw new Error(`UICheckBox '${this.widgetName}' failed to load: custom base-template does not contains an <div/> with Id="UICheckBox".`)
            if (this.customBaseTemplate.indexOf('checkElement') == -1)
                throw new Error(`UICheckBox '${this.widgetName}' failed to load: custom base-template does not contains an <input/> with Id="checkElement".`)
            if (this.customBaseTemplate.indexOf('checkLabel') == -1)
                throw new Error(`UICheckBox '${this.widgetName}' failed to load: custom base-template does not contains an <label/> with Id="checkLabel".`)

            return this.customBaseTemplate;
        }

        if (PageShell.BOOTSTRAP_VERSION_NUMBER > 4.9)
            throw new Error(`UICheckBox: this Widget does dot supports Bootstrap's v${PageShell.BOOTSTRAP_VERSION}; For use with v5.x, you should use 'UICheckBoxBS5' class. `)
        return `
<div id="UICheckBox" class="custom-control custom-checkbox">
  <input id="checkElement" class="custom-control-input" type="checkbox" value="">
  <label id="checkLabel" class="custom-control-label font-weight-normal" for="checkElement">
    Default checkbox
  </label>
</div>`
    }

    protected onWidgetDidLoad(): void
    {
        var self = this;
        self.divContainer = self.elementById('UICheckBox');
        self.checkElement = self.elementById('checkElement');
        self.checkLabel = self.elementById('checkLabel');
        self.checkLabel.htmlFor = self.checkElement.id;
        self.checkLabel.textContent = self.labelText;
        self.checkElement.checked = self.initialChecked;

        self.checkElement.onchange = function (ev)
        {
            if (self.onCheckedChange != null) self.onCheckedChange({ checked: self.checkElement.checked, checkBox: self });
        };
    }


    /**
     * 
     * @param fnOnChange 
     * ```
     * (checked: boolean, checkBox: UICheckBox) => {  }
     * ```
     */
    public setOnCheckChange(fnOnChange: Function)
    {
        const $ = this;
        this.onCheckedChange = fnOnChange
        this.checkElement.onchange = () =>
        {
            if (!Misc.isNull($.onCheckedChange))
                $.onCheckedChange($.value(), $)
        }
    }

    public setText(text: string): void
    {
        this.labelText = text;
        this.checkLabel.textContent = this.labelText;
    }

    public setCustomPresenter(renderer: ICustomWidgetPresenter<Widget>): void
    {
        renderer.render(this);
    }
    public value(): string
    {
        return this.checkElement.checked.toString();
    }
    public setEnabled(enabled: boolean): void
    {
        this.checkElement.disabled = (enabled == false);
    }
    public addCSSClass(className: string): void
    {
        this.checkElement.classList.add(className);
    }
    public removeCSSClass(className: string): void
    {
        this.checkElement.classList.remove(className);
    }
    public applyCSS(propertyName: string, propertyValue: string): void
    {
        this.checkElement.style.setProperty(propertyName, propertyValue);
    }
    public setPosition(position: string,
        marginLeft: string,
        marginTop: string,
        marginRight: string,
        marginBottom: string,
        transform?: string): void
    {
        this.divContainer.style.position = position;
        this.divContainer.style.left = marginLeft;
        this.divContainer.style.top = marginTop;
        this.divContainer.style.right = marginRight;
        this.divContainer.style.bottom = marginBottom;
        this.divContainer.style.transform = `${transform}`;
    }

    public setVisible(visible: boolean): void
    {
        this.divContainer.style.visibility = (visible ? 'visible' : 'hidden');
    }

    public setChecked(isChecked: boolean): void
    {
        this.checkElement.checked = isChecked;
        if (!Misc.isNull(this.onCheckedChange))
            this.onCheckedChange(isChecked, this)
    }
    public isChecked(): boolean
    {
        return this.checkElement.checked;
    }
}
/**
 * Exclusive Bootstrap v5.x CheckBox compat.
 */
export class UICheckBoxBS5 extends UICheckBox
{
    constructor({ name, text, checked = false}:
        {
            name: string;
            text: string;
            checked?: boolean;
        })
    {
        const customTemplate = `
        <div id="UICheckBox" class="form-check">
            <input id="checkElement" class="form-check-input" type="checkbox" value="">
            <label id="checkLabel" class="form-check-label" for="checkLabel">
                Checked checkbox
            </label>
        </div> `;

        super({ name, text, checked, customTemplate });
    }
}
export class UIImageBinder extends WidgetBinder
{
    private img: UIImage;
    constructor(image: UIImage)
    {
        super(image);
        this.img = image;
    }
    getWidgetValue()
    {
        return this.img.value();
    }
    refreshUI(): void
    {
        var valueModel = this.getModelPropertyValue();
        this.img.setSource(valueModel);
    }
    fillPropertyModel(): void { }
}

export class UIImage extends Widget implements IBindable
{
    public image: HTMLImageElement;

    private imgSrc: string;
    private imgAlt: string;
    private imgCssClass: string;
    width: string;
    height: string;

    constructor({ name, src, cssClass, alt, width, height }:
        {
            name: string,
            src?: string,
            cssClass?: string,
            alt?: string,
            width?: string,
            height?: string
        })
    {
        super(name);

        if (cssClass == null)
            cssClass = 'img-fluid'

        this.imgCssClass = cssClass;
        this.imgSrc = src;
        this.imgAlt = `${alt}`;
        this.width = width;
        this.height = height;
    }
    getBinder(): WidgetBinder
    {
        return new UIImageBinder(this);
    }

    protected htmlTemplate(): string
    {
        return `<img id="fsImageView" src="" class="img-fluid" alt="">`;
    }

    protected onWidgetDidLoad(): void
    {
        this.image = this.elementById('fsImageView');

        if (!Misc.isNullOrEmpty(this.width))
            this.image.style.width = this.width

        if (!Misc.isNullOrEmpty(this.height))
            this.image.style.height = this.height

        this.image.alt = this.imgAlt;
        this.setSource(this.imgSrc);

        if (!Misc.isNullOrEmpty(this.imgCssClass))
        {
            var classes = this.imgCssClass.split(' ');
            for (var c = 0; c < classes.length; c++)
                this.addCSSClass(classes[c])
        }
    }

    public setSource(imgSource: string)
    {
        this.imgSrc = imgSource;
        this.image.src = this.imgSrc;
    }

    public setCustomPresenter(renderer: ICustomWidgetPresenter<Widget>): void
    {
        renderer.render(this);
    }
    public value(): string
    {
        return this.imgSrc;
    }
    public setEnabled(enabled: boolean): void
    {
        throw new Error("Method not implemented.");
    }
    public addCSSClass(className: string): void
    {
        this.image.classList.add(className);
    }
    public removeCSSClass(className: string): void
    {
        this.image.classList.remove(className);
    }
    public applyCSS(propertyName: string, propertyValue: string): void
    {
        this.image.style.setProperty(propertyName, propertyValue);
    }
    public setPosition(position: string, marginLeft: string, marginTop: string, marginRight: string, marginBottom: string, transform?: string): void
    {
        this.image.style.position = position;
        this.image.style.left = marginLeft;
        this.image.style.top = marginTop;
        this.image.style.right = marginRight;
        this.image.style.bottom = marginBottom;
        this.image.style.transform = `${transform}`;
    }
    public setVisible(visible: boolean): void
    {
        this.image.style.visibility = (visible ? 'visible' : 'hidden')
    }
}
export class UILabelBinder extends WidgetBinder
{
    private label: UILabel;
    constructor(label: UILabel)
    {
        super(label);
        this.label = label;
    }

    refreshUI(): void
    {
        var value = this.getModelPropertyValue();
        this.label.setText(`${value}`);
    }
    fillPropertyModel(): void
    {
        var text: string = this.label.getText();
        this.setModelPropertyValue(text);
    }
    getWidgetValue()
    {
        var text: string = this.label.getText();
        return text;
    }
}

export class UILabel extends Widget implements IBindable
{
    private lblText: string;
    private cssClass: string = 'label';

    constructor({ name, text, cssClass = 'label' }:
        {
            name: string,
            text: string,
            cssClass?: string
        })
    {

        super(name);
        this.lblText = text;

        if (!Misc.isNullOrEmpty(cssClass))
            this.cssClass = cssClass;
    }

    public label: HTMLLabelElement;
    protected htmlTemplate(): string
    {
        return `<label id="uiLabel" class="${this.cssClass}"> Default label </label>`;
    }

    protected onWidgetDidLoad(): void
    {
        this.label = this.elementById('uiLabel');
        this.label.textContent = this.lblText;
    }
    public setText(text: string): void
    {
        this.label.textContent = text;
    }

    public getText(): string
    {
        return this.value();
    }



    getBinder(): WidgetBinder
    {
        return new UILabelBinder(this);
    }

    public setCustomPresenter(renderer: ICustomWidgetPresenter<Widget>): void
    {
        renderer.render(this);
    }
    public value(): string
    {
        return `${this.label.textContent}`;
    }

    public setEnabled(enabled: boolean): void
    {
        throw new Error("Method not implemented.");
    }
    public addCSSClass(className: string): void
    {
        this.label.classList.add(className);
    }
    public removeCSSClass(className: string): void
    {
        this.label.classList.add(className);
    }
    public applyCSS(propertyName: string, propertyValue: string): void
    {
        this.label.style.setProperty(propertyName, propertyValue);
    }
    public setPosition(position: string, marginLeft: string, marginTop: string, marginRight: string, marginBottom: string, transform?: string): void
    {
        this.label.style.position = position;
        this.label.style.left = marginLeft;
        this.label.style.top = marginTop;
        this.label.style.right = marginRight;
        this.label.style.bottom = marginBottom;
        this.label.style.transform = `${transform}`;
    }
    public setVisible(visible: boolean): void
    {
        this.label.style.visibility = (visible ? 'visible' : 'hidden')
    }
}
export class UIListBinder extends WidgetBinder
{
    private listView: UIList;

    constructor(listView: UIList)
    {
        super(listView);
        this.listView = listView;
    }
    refreshUI(): void
    {
        var viewModels: Array<any> = this.getModelPropertyValue();
        this.listView.fromList(viewModels, this.valueProperty, this.displayProperty);

        if (this.isTargetDefined())
        {
            var value = this.getModelTargetPropertyValue();
            this.listView.setSelectedValue(value);
        }
    }
    getWidgetValue()
    {
        var item = this.listView.selectedItem();
        if (item == null) return null;
        return item.value;
    }
    fillPropertyModel(): void
    {
        if (this.isTargetDefined())
        {
            this.fillModelTargetPropertyValue();
        }
    }
}

export class UIList extends Widget implements IBindable
{
    protected htmlTemplate(): string
    {
        return `
<div id="fsListView" class="list-group ${this.containerDivClass}">
</div>`
    }
    public items: Array<IListItemTemplate> = [];
    public divContainer: HTMLDivElement;

    private itemClickedCallback: Function;

    private templateProvider: IListItemTemplateProvider;

    public customBehaviorColors = false;
    public unSelectedBackColor: string = null;
    public unSelectedForeColor: string = null;
    public selectedBackColor: string = null;
    public selectedForeColor: string = null;

    disableSel: boolean = false;
    disableUnSel: boolean = false;
    containerDivClass: string = ''


    /**
     * 
     * @param itemClicked Function to handle onClick item event. 
     * 
     * Parameters: **(item: IListItemTemplate, ev: Event)**
     */
    constructor({ name, multiSelect, containerClass }:
        {
            name: string,
            multiSelect?: boolean,
            containerClass?: string
        })
    {
        super(name);
        this.containerDivClass = containerClass
        this.multiSelect = (Misc.isNull(multiSelect) ? false : multiSelect)
    }

    public disableSelection(): UIList
    {
        this.disableSel = true;
        return this;
    }

    public disableUnSelection(): UIList
    {
        this.disableUnSel = true;
        return this;
    }

    /**
     * 
     * @param fnClick 
     * ```
     *  function onItemClicked(item: IListItemTemplate) { }
     * ```
     */
    public setItemClickFn(fnClick: Function)
    {
        this.itemClickedCallback = fnClick;
    }

    public setTemplateProvider(itemTemplateProvider: IListItemTemplateProvider)
    {
        this.templateProvider = itemTemplateProvider;
    }

    /**
     * Changes the color selection behavior for each UIList item. 
     * 
     * NOTE: not every implementation of 'IListItemTemplate'
     * will be able to obey this
     */
    public changeColors(selectedBack: string, selectedFore: string,
        unSelectedBack: string, unSelectedFore: string)
    {
        this.customBehaviorColors = true;
        this.selectedBackColor = selectedBack;
        this.selectedForeColor = selectedFore;
        this.unSelectedBackColor = unSelectedBack;
        this.unSelectedForeColor = unSelectedFore;
    }

    public itemTemplateProvider(): IListItemTemplateProvider
    {
        return this.templateProvider;
    }

    getBinder(): WidgetBinder
    {
        return new UIListBinder(this);
    }

    public fromList(viewModels: Array<any>, valueProperty?: string, displayProperty?: string): void
    {
        this.divContainer.innerHTML = '';
        if (viewModels == null || viewModels == undefined || viewModels.length == 0) 
        {
            try
            {
                var templateProvider = this.itemTemplateProvider();
                if (templateProvider != null)
                {
                    var customItem = templateProvider.getListItemTemplate(this, null);
                    if (customItem != null && customItem != undefined)
                        this.addItem(customItem);
                }
            } catch (err: any | object | Error)
            {
                console.error(err);
            }
            return;
        };

        for (var i = 0; i < viewModels.length; i++)
        {
            var viewModel: any | object = viewModels[i];
            var text = (displayProperty == null ? `${viewModel}` : viewModel[displayProperty]);
            var value = (valueProperty == null ? `${viewModel}` : viewModel[valueProperty]);

            if (this.itemTemplateProvider() == null)
            {
                var defaultItemTemplate = new ListItem(
                    `${i + 1}`,
                    text,
                    value);

                defaultItemTemplate.viewModel = viewModels[i];

                this.addItem(defaultItemTemplate);
            }
            else
            {
                var templateProvider = this.itemTemplateProvider();
                var customItem = templateProvider.getListItemTemplate(this, viewModel);
                this.addItem(customItem);
            }
        }
    }

    protected onWidgetDidLoad(): void
    {
        this.divContainer = this.elementById('fsListView');
    }

    public multiSelect = false

    public onItemClicked(senderItem: IListItemTemplate, ev: Event): void
    {
        if (this.multiSelect)
        {
            if (senderItem.isSelected()) senderItem.unSelect();
            else senderItem.select();
        } else
        {
            if (this.disableUnSel == false)
                for (var i = 0; i < this.items.length; i++)
                    this.items[i].unSelect();

            if (this.disableSel == false)
                senderItem.select();
        }

        if (this.itemClickedCallback != null && this.itemClickedCallback != undefined)
            this.itemClickedCallback(senderItem, ev);
    }

    public addItem(item: IListItemTemplate): UIList
    {
        item.setOwnerList(this);
        this.items.push(item);
        var view: HTMLAnchorElement = item.itemTemplate();

        var self = this;
        view.onclick = function (ev)
        {
            self.onItemClicked(item, ev);
        };

        this.divContainer.append(view);
        return this;
    }

    public removeItem(item: IListItemTemplate): void
    {
        for (var i = 0; i < this.divContainer.children.length; i++)
        {
            var view: HTMLAnchorElement = this.divContainer.children[i] as HTMLAnchorElement;
            if (view.id == item.itemName)
            {
                var indx = this.items.indexOf(item);
                if (indx >= 0) this.items.splice(indx, 1);

                this.divContainer.removeChild(view);
                item = null;
                return;
            }
        }
    }

    public setSelectedValue(itemValue: any): void
    {
        for (var i = 0; i < this.items.length; i++)
        {
            var item = this.items[i];
            if (Misc.isNull(itemValue))
                item.unSelect()
            else
            {
                if (item.value == itemValue)
                {
                    item.select();
                    if (this.itemClickedCallback != null && this.itemClickedCallback != undefined)
                        this.itemClickedCallback(itemValue);
                }
                else
                    item.unSelect();
            }
        }
    }

    public setSelectedItem(selectedItem: IListItemTemplate): void
    {
        for (var i = 0; i < this.items.length; i++)
        {
            var item = this.items[i];
            item.unSelect();
        }
        if (!Misc.isNull(selectedItem))
        {
            selectedItem.select();
            if (this.itemClickedCallback != null && this.itemClickedCallback != undefined)
                this.itemClickedCallback(selectedItem);
        }
    }

    public selectedItem(): IListItemTemplate
    {
        for (var i = 0; i < this.items.length; i++)
        {
            var item = this.items[i];
            if (item.isSelected())
                return item;
        }
        return null;
    }
    public selectedValue(): any | object
    {
        var sItem = this.selectedItem();
        if (sItem == null || sItem == undefined) return null;
        return sItem.value;
    }
    public setCustomPresenter(renderer: ICustomWidgetPresenter<Widget>): void
    {
        renderer.render(this);
    }
    public value(): string
    {
        return this.selectedValue();
    }
    public setEnabled(enabled: boolean): void
    {
        throw new Error("Method not implemented.");
    }
    public addCSSClass(className: string): void
    {
        this.divContainer.classList.add(className);
    }
    public removeCSSClass(className: string): void
    {
        this.divContainer.classList.remove(className);
    }

    public applyCSS(propertyName: string, propertyValue: string): void
    {
        this.divContainer.style.setProperty(propertyName, propertyValue);
    }
    public setPosition(position: string, marginLeft: string, marginTop: string, marginRight: string, marginBottom: string, transform?: string): void
    {
        this.divContainer.style.position = position;
        this.divContainer.style.left = marginLeft;
        this.divContainer.style.top = marginTop;
        this.divContainer.style.right = marginRight;
        this.divContainer.style.bottom = marginBottom;
        this.divContainer.style.transform = transform;
    }
    public setVisible(visible: boolean): void
    {
        this.divContainer.style.visibility = (visible ? 'visible' : 'hidden')
    }

    public setOnItemClick(itemClicked: Function)
    {
        this.itemClickedCallback = itemClicked;
    }
}
export class UIDialog extends Widget implements INotifiable
{
    public static $: UIDialog;

    private showFunction: VirtualFunction;

    public contentTemplate: UITemplateView;
    private modalActions: ModalAction[] = [];
    private titleText: string;

    public modalContainer: HTMLDivElement;
    public titleElement: HTMLHeadElement;
    public bodyContainer: HTMLDivElement;
    public footerContainer: HTMLDivElement;
    public btnClose: HTMLButtonElement;
    public modalContent: HTMLDivElement;

    private shell: PageShell;

    private modalContext: WidgetContext;

    private height: string = null;

    private customTemplate: string = null;
    onCloseFn: Function;
    constructor(shell: PageShell, customTempl?: string, height?: string)
    {
        super('UIDialog');

        this.shell = shell;
        this.height = height;
        if (!Misc.isNullOrEmpty(customTempl))
            this.customTemplate = customTempl;
        else
        {
            if (PageShell.BOOTSTRAP_VERSION_NUMBER >= 5)
                throw new DefaultExceptionPage(new Error(`UIDialog: this widget does not supports Bootstrap v${PageShell.BOOTSTRAP_VERSION}. Use 'UIDialogBS5' class instead it.`))
        }

        // obtem o body da pagina
        var body: Element = shell.getPageBody();

        // verifica se existe a div que vai conter o modal
        var modalDivContainer: Element = shell.elementById('modalContainer');
        if (modalDivContainer == null)
        {
            // nao existe, então deve ser criada uma div-container 
            // para controlar o modal
            modalDivContainer = shell.createElement('div');
            modalDivContainer.id = 'modalContainer';
            body.appendChild(modalDivContainer);
        }

        // é criado um WidgetContext para gerenciar a div
        // container do modal
        this.modalContext = new WidgetContext(
            shell,
            [modalDivContainer.id],
            null);
    }

    public closeDialog()
    {
        this.modalContainer.remove();
        UIDialog.$ = null;
    }

    public action(action: ModalAction): UIDialog
    {
        this.modalActions.push(action);
        return this;
    }

    public setTitle(dialogTitle: string): UIDialog
    {
        this.titleText = dialogTitle;
        return this;
    }

    public modalBody(templateView: UITemplateView): UIDialog
    {
        this.contentTemplate = templateView;
        return this;
    }

    public setText(dialogText: string): UIDialog
    {
        this.contentTemplate = new UITemplateView(dialogText, this.shell);
        return this;
    }

    public useTemplate(templateView: UITemplateView): UIDialog
    {
        this.contentTemplate = templateView;
        return this;
    }

    private dataDismissAttrName: string = 'data-dismiss'
    public setDataDismisAttributeName(attrName: string)
    {
        this.dataDismissAttrName = attrName;
    }

    protected htmlTemplate(): string
    {
        if (!Misc.isNullOrEmpty(this.customTemplate))
        {
            if (this.customTemplate.indexOf('UIModalView') == -1)
                throw new Error(`UIDialog '${this.widgetName}' failed to load: custom base-template does not contains an <div/> with Id="UIModalView".`)
            if (this.customTemplate.indexOf('modalTitle') == -1)
                throw new Error(`UIDialog '${this.widgetName}' failed to load: custom base-template does not contains an Element with Id="modalTitle".`)
            if (this.customTemplate.indexOf('modalBody') == -1)
                throw new Error(`UIDialog '${this.widgetName}' failed to load: custom base-template does not contains an <div/> with Id="modalBody".`)
            if (this.customTemplate.indexOf('modalFooter') == -1)
                throw new Error(`UIDialog '${this.widgetName}' failed to load: custom base-template does not contains an <div/> with Id="modalFooter".`)

            return this.customTemplate;
        }
        var styleHeight: string = '';
        if (!Misc.isNullOrEmpty(this.height))
            styleHeight = `style="height:${this.height}"`
        return `
 <div id="UIModalView" class="modal fade" role="dialog">
    <div class="modal-dialog" role="document">        
        <div id="modalContent" class="modal-content shadow-lg" ${styleHeight}>
            <div class="modal-header">
                <h5 id="modalTitle" class="modal-title">Modal title</h5>
                <button id="btnClose" type="button" class="close" ${this.dataDismissAttrName}="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            
            <div id="modalBody" class="modal-body pt-1" style="background:white">
                
            </div>

            <div id="modalFooter" class="modal-footer">
        
            </div>
        </div>
    </div>
  </div>`;

    }

    public setHeight(height: string): UIDialog
    {
        this.height = height
        if (!Misc.isNull(this.modalContent))
            this.modalContent.style.height = height;
        return this;
    }

    protected onWidgetDidLoad(): void
    {
        var self = this;

        self.modalContainer = self.elementById('UIModalView');
        self.modalContent = self.elementById('modalContent')
        self.titleElement = self.elementById('modalTitle');
        self.bodyContainer = self.elementById('modalBody');
        self.footerContainer = self.elementById('modalFooter');
        self.btnClose = self.elementById('btnClose');
        self.titleElement.textContent = self.titleText;

        if (!Misc.isNullOrEmpty(self.contentTemplate))
            self.bodyContainer.appendChild(self.contentTemplate.content());

        for (var i = 0; i < self.modalActions.length; i++)
        {
            const action: ModalAction = self.modalActions[i];
            const btn: HTMLButtonElement = self.shell.createElement('button');
            btn.type = 'button';
            btn.id = `modalAction_${Misc.generateUUID()}`;
            btn.textContent = action.text;

            for (var c = 0; c < action.classes.length; c++)
                btn.classList.add(action.classes[c]);

            action.setButton(btn, this);
            if (action.dismis)
                btn.setAttribute(`${this.dataDismissAttrName}`, 'modal');

            self.footerContainer.appendChild(btn);
        }

        self.showFunction = new VirtualFunction({
            fnName: 'modalShow',
            fnArgNames: [
                'containerId',
                'showFunctionId'
            ],
            keepAfterCalled: true
        })
        self.showFunction.setContent(`
            var md = new bootstrap.Modal(document.getElementById(containerId), { backdrop: false })
            md.show();
            var refId = ('#' + containerId)
            $(refId).on('hidden.bs.modal', function (e) {
                document.getElementById(containerId).remove();
                document.getElementById(showFunctionId).remove();
            })
        `).call(self.modalContainer.id, self.showFunction.functionId);
    }

    private onComplete: Function = null;
    public show(onComplete?: Function): void
    {
        this.onComplete = onComplete;
        this.modalContext.addWidget('modalContainer', this);
        this.modalContext.build(this);
        UIDialog.$ = this;
    }

    public setOnCloseFn(onClose: Function)
    {
        this.onCloseFn = onClose;
        this.btnClose.onclick = () => onClose
    }

    onNotified(sender: any, args: Array<any>): void
    {
        if (Misc.isNull(this.onComplete) == false)
            this.onComplete(this);
    }

    public setCustomPresenter(renderer: ICustomWidgetPresenter<Widget>): void
    {
        renderer.render(this);
    }
}
export class UIDialogBS5 extends UIDialog
{
  public static $: UIDialogBS5;
  constructor(shell: PageShell, height: string = '')
  {
    if (PageShell.BOOTSTRAP_VERSION_NUMBER < 5)
      throw new DefaultExceptionPage(new Error(`UIDialogBS5: this widget does not supports Bootstrap v${PageShell.BOOTSTRAP_VERSION}. Use 'UIDialog' class instead it.`))

    const tmpl = `
<div id="UIModalView" class="modal" tabindex="-1" >
  <div class="modal-dialog modal-dialog-scrollable">
    <div id="modalContent" class="modal-content shadow-lg" ${Misc.isNullOrEmpty(height) ? '' : `style="height:${height}"`}>
      <div class="modal-header">
        <h5   id="modalTitle" class="modal-title">Modal title</h5>
        <button id="btnClose" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div id="modalBody" class="modal-body pt-1" style="background:white">

      </div>
      <div id="modalFooter" class="modal-footer">
   
      </div>
    </div>
  </div>
</div> `

    super(shell, tmpl)
    this.setDataDismisAttributeName('data-bs-dismiss')
    UIDialogBS5.$ = this;
    UIDialog.$ = this;
  }
}
export class UINavBar extends Widget
{

    public navBar: HTMLDivElement;
    public leftLinks: HTMLUListElement;
    public rightLinks: HTMLUListElement;
    public brandText: HTMLAnchorElement;
    public pushMenuButton: HTMLAnchorElement;

    constructor(name: string)
    {
        super(name);
    }

    protected htmlTemplate(): string
    {
        return `
<nav id="fsNavbar" class="navbar fixed-top">
  
    <!-- Left navbar links -->
    <ul id="navLeftLinks" class="navbar-nav">
        <li class="nav-item">
           <a id="btnPushMenu" class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a>
        </li>
       
     
    </ul>

   <a id="brandText" class="navbar-brand">My First App</a>

    <!-- Right navbar links -->
    <ul id="navRightLinks" class="navbar-nav ml-auto">
    </ul>
</nav>`
    }

    onWidgetDidLoad(): void
    {
        this.navBar = this.elementById('fsNavbar');
        this.leftLinks = this.elementById('navLeftLinks')
        this.rightLinks = this.elementById('navRightLinks')
        this.brandText = this.elementById('brandText');
        this.pushMenuButton = this.elementById('btnPushMenu');

        this.pushMenuButton.style.marginLeft = '5px';
        this.brandText.style.marginLeft = '10px';
        this.navBar.style.boxShadow = '0 0 1em lightgray';
    }

    public setEnabled(enabled: boolean): void
    {
        throw new Error("Method not implemented.");
    }
    public value(): string
    {
        throw new Error("Method not implemented.");
    }
    public addCSSClass(className: string): void
    {
        this.navBar.classList.add(className);
    }

    public removeCSSClass(className: string): void
    {
        this.navBar.classList.remove(className);
    }

    public applyCSS(propertyName: string, propertyValue: string): void
    {
        this.navBar.style.setProperty(propertyName, propertyValue);
    }
    public setPosition(position: string, marginLeft: string, marginTop: string, marginRight: string, marginBottom: string, transform?: string): void
    {
        this.navBar.style.position = position;
        this.navBar.style.left = marginLeft;
        this.navBar.style.top = marginTop;
        this.navBar.style.right = marginRight;
        this.navBar.style.bottom = marginBottom;
        this.navBar.style.transform = transform;
    }
    public setVisible(visible: boolean): void
    {
        this.navBar.hidden = (visible == false);
    }
    public setCustomPresenter(renderer: ICustomWidgetPresenter<Widget>): void
    {
        renderer.render(this);
    }


}
export class UIProgressBar extends Widget
{
    public divContainer: HTMLDivElement
    public divProgressBar: HTMLDivElement
    backgroundCssClass: string;
    containerCssClass: string;
    initialVisibleState: boolean;

    /**
     *
     */
    constructor({ name, backgroundCssClass, containerCssClass, visible }: {
        name: string,
        backgroundCssClass?: string,
        containerCssClass?: string,
        visible?: boolean
    })
    {
        super(name);

        this.backgroundCssClass = backgroundCssClass ?? 'bg-primary';
        this.containerCssClass = containerCssClass ?? 'col';
        this.initialVisibleState = visible ?? true
    }

    protected htmlTemplate(): string
    {
        return `
<div id="UIProgressBar" class="progress ${this.containerCssClass}">
  <div id="divProgressBar" class="progress-bar ${this.backgroundCssClass}" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
        `
    }
    protected onWidgetDidLoad(): void
    {
        this.divContainer = this.elementById('UIProgressBar');
        this.divProgressBar = this.elementById('divProgressBar');

        this.setVisible(this.initialVisibleState)
    }

    public setValue(value: number): void
    {
        if (value > 100) value = 100
        this.divProgressBar.style.width = `${value}%`;
        this.divProgressBar.ariaValueNow = value.toString();
    }

    public value(): number
    {
        return Number.parseInt(this.divProgressBar.ariaValueNow);
    }

    public override setVisible(visible: boolean): void
    {
        this.divContainer.style.visibility = (visible ? 'visible' : 'hidden');
    }
}
export class UISelectBinder extends WidgetBinder
{
    private select: UISelect;
    constructor(select: UISelect)
    {
        super(select);
        this.select = select;
    }
    getWidgetValue()
    {
        return this.select.value();
    }
    refreshUI(): void
    {
        var models: Array<any | object> = this.getModelPropertyValue();
        if (this.bindingHasPath)
            this.select.fromList(models, this.valueProperty, this.displayProperty);
        else
            this.select.fromList(models);

        if (this.isTargetDefined())
        {
            var value = this.getModelTargetPropertyValue();
            this.select.setSelectedOption(value);
        }
    }
    fillPropertyModel(): void
    {
        if (this.isTargetDefined())
        {
            this.fillModelTargetPropertyValue();
        }
    }
}

export class UISelect extends Widget implements IBindable
{
    protected htmlTemplate(): string
    {
        return `
<div id="fsSelect" class="form-group">
    <label id="selectTitle" style="margin: 0px; padding: 0px; font-weight:normal !important;" for="selectEl"> Select Title </label>
    <select style="height: 35px" id="selectEl" class="form-control">
    </select>
</div>`
    }

    public divContainer: HTMLDivElement = null;
    public title: HTMLLabelElement = null;
    public select: HTMLSelectElement = null;

    public onSelectionChanged: Function = null;
    private initialTitle: string = null;

    private containerClass: string = null;

    /**
     * 
     * @param onChangeFn 
     * ```
     * (value: any, select: UISelect) => { } 
     * ```
    */
    constructor({ name, title, containerClass, selectionChangeFn }:
        {
            name: string,
            title: string,
            containerClass?: string,
            selectionChangeFn?: Function
        })
    {
        super(name);
        this.initialTitle = title;
        this.containerClass = containerClass;
        this.onSelectionChanged = selectionChangeFn;
    }
    getBinder(): WidgetBinder
    {
        return new UISelectBinder(this);
    }

    protected onWidgetDidLoad(): void
    {
        var self = this;

        this.divContainer = this.elementById('fsSelect');
        this.title = this.elementById('selectTitle');
        this.select = this.elementById('selectEl');

        if (!Misc.isNull(this.containerClass))
        {
            var classes = this.containerClass.split(' ');
            for (var c = 0; c < classes.length; c++)
                this.divContainer.classList.add(classes[c]);
        }

        const $ = this
        this.select.onchange = function (ev)
        {
            if (!Misc.isNull($.onSelectionChanged))
                $.onSelectionChanged($.value(), $)
        };
        this.title.textContent = this.initialTitle;

    }

    /**
     * 
     * @param onChangeFn 
     * ```
     * (value: any, select: UISelect) => { } 
     * ```
     */
    public setOnChange(onChangeFn: Function)
    {
        const $ = this;
        this.onSelectionChanged = onChangeFn
        this.select.onselectionchange = () =>
        {
            if (!Misc.isNull($.onSelectionChanged))
                $.onSelectionChanged($.value(), $)
        }
    }


    public setSelectedOption(optionValue: any): void
    {
        try
        {
            for (var i = 0; i < this.select.options.length; i++)
                this.select.options[i].selected = false;

            for (var i = 0; i < this.select.options.length; i++)
            {
                var option = this.select.options[i];

                if (option.value == optionValue)
                {
                    option.selected = true;
                    if (!Misc.isNull(this.onSelectionChanged))
                        this.onSelectionChanged(option.value, this)
                    return;
                }
            }
        } catch (error)
        {
            this.processError(error);
        }
    }

    private itemsSource: Array<any> = [];
    valueProperty?: string;
    displayProperty?: string;

    public fromList(models: Array<any>,
        valueProperty?: string,
        displayProperty?: string): void
    {
        if (models == null || models == undefined) return;
        try
        {
            this.valueProperty = valueProperty;
            this.displayProperty = displayProperty;
            this.itemsSource = models;
            var optionsFromModels: Array<SelectOption> = [];
            for (var i = 0; i < models.length; i++)
            {
                var model = models[i];
                var option: SelectOption = null;

                if (valueProperty != null && valueProperty != undefined)
                    option = new SelectOption(model[valueProperty], model[displayProperty])
                else
                    option = new SelectOption(`${models[i]}`, `${models[i]}`);

                optionsFromModels.push(option);
            }
            this.addOptions(optionsFromModels);
        }
        catch (error)
        {
            this.processError(error);
        }
    }

    public addOptions(options: Array<SelectOption>): void
    {
        this.select.innerHTML = '';
        for (var i = 0; i < options.length; i++)
            this.addOption(options[i]);
    }

    public addOption(option: SelectOption): UISelect
    {
        try
        {
            var optionEL: HTMLOptionElement = document.createElement('option');
            optionEL.value = option.value;
            optionEL.textContent = option.text;
            this.select.add(optionEL);
            return this;
        }
        catch (error)
        {
            this.processError(error);
        }
    }

    public setTitle(title: string): void
    {
        this.title.textContent = title;
    }

    public setCustomPresenter(renderer: ICustomWidgetPresenter<Widget>): void
    {
        try
        {
            renderer.render(this);
        }
        catch (error)
        {
            this.processError(error);
        }
    }

    /**
     * Gets the selected object-value item
     * Its works only when uses 'fromList(...)' UISelect function
     * @returns T
     */
    public getSelectedItem<T extends any | object>(): T
    {
        var val = this.value(); //key value of object-item
        for (var i = 0; i < this.itemsSource.length; i++)
        {
            var itemObj = this.itemsSource[i];
            if (itemObj[this.valueProperty] == val)
                return itemObj as unknown as T;
        }
        return null;
    }

    public value(): any
    {
        return this.select.value;
    }
    public addCSSClass(className: string): void
    {
        this.select.classList.add(className);
    }

    public removeCSSClass(className: string): void
    {
        this.select.classList.remove(className);
    }
    public applyCSS(propertyName: string, propertyValue: string): void
    {
        this.select.style.setProperty(propertyName, propertyValue);
    }
    public setPosition(position: string, marginLeft: string, marginTop: string, marginRight: string, marginBottom: string, transform?: string): void
    {
        this.divContainer.style.position = position;
        this.divContainer.style.left = marginLeft;
        this.divContainer.style.top = marginTop;
        this.divContainer.style.right = marginRight;
        this.divContainer.style.bottom = marginBottom;
        this.divContainer.style.transform = transform;
    }
    public setVisible(visible: boolean): void
    {
        this.divContainer.style.visibility = (visible ? 'visible' : 'hidden')
    }

    public setEnabled(enabled: boolean): void
    {
        this.select.disabled = (enabled == false);
    }
}
export class UISpinner extends Widget
{
    private colorCls: string;
    private initialVisible: boolean;

    public containerDiv: HTMLDivElement = null;
    public spanSpinner: HTMLSpanElement = null;

    constructor({ name, colorClass = 'text-primary', visible = true }:
        {
            name: string,
            colorClass?: string,
            visible?: boolean
        })
    {
        super(name);


        this.colorCls = (Misc.isNullOrEmpty(colorClass) ? 'text-primary' : colorClass);
        this.initialVisible = (Misc.isNull(visible) ? true : visible);
    }
    private customTemplate: string = '';

    protected defineTemplate(templateStr: string)
    {
        this.customTemplate = templateStr;
    }

    protected onWidgetDidLoad(): void
    {
        if (Misc.isNullOrEmpty(this.customTemplate))
            if (PageShell.BOOTSTRAP_VERSION_NUMBER < 5)
                console.error(new Error(`UISpinner: this widget does not supports Bootstrap v${PageShell.BOOTSTRAP_VERSION}. Use 'UISpinnerBS5' class instead it.`))

        this.containerDiv = this.elementById('container');
        this.spanSpinner = this.elementById('spnSpinner');

        this.setVisible(this.initialVisible);
    }
    protected htmlTemplate(): string
    {
        var colorClass = this.colorCls;
        if (colorClass == 'primary') colorClass = 'text-primary';
        if (colorClass == '') colorClass = 'text-primary';

        if (!Misc.isNullOrEmpty(this.customTemplate))
            return this.customTemplate;

        return `
<div id="container" class="spinner-border ${colorClass}" role="status">
    <span id="spnSpinner" class="sr-only"/>
</div>
        `
    }
    public setCustomPresenter(renderer: ICustomWidgetPresenter<Widget>): void
    {
        renderer.render(this);
    }
    public value(): string
    {
        return null;
    }
    public setEnabled(enabled: boolean): void
    {

    }
    public addCSSClass(className: string): void
    {
        this.spanSpinner.classList.remove(className);
    }
    public removeCSSClass(className: string): void
    {
        this.spanSpinner.classList.add(className);
    }
    public applyCSS(propertyName: string, propertyValue: string): void
    {
        this.spanSpinner.style.setProperty(propertyName, propertyValue);
    }
    public setPosition(position: string, marginLeft: string, marginTop: string, marginRight: string, marginBottom: string, transform?: string): void
    {

    }
    public setVisible(visible: boolean): void
    {
        this.containerDiv.style.visibility = (visible ? 'visible' : 'hidden')
    }

}
/**
 * UISpinner portado para Bootstrao 5
 */
export class UISpinnerBS5 extends UISpinner
{
    constructor({ name, colorClass = 'text-primary', visible = true }:
        {
            name: string,
            colorClass?: string,
            visible?: boolean
        })
    {

        super({ name, colorClass, visible })

        this.defineTemplate(`
<div id="container" class="spinner-border ${colorClass}" role="status">
    <span id="spnSpinner" class="visually-hidden">Loading...</span>
</div>
        `)
    }

    protected override onWidgetDidLoad(): void
    {
        if (PageShell.BOOTSTRAP_VERSION_NUMBER < 5)
            console.error(new Error(`UISpinnerBS5: this widget does not supports Bootstrap v${PageShell.BOOTSTRAP_VERSION}. Use 'UISpinner' class instead it.`))

        super.onWidgetDidLoad()
    }
}
export class UISwitcher extends Widget
{
    protected onWidgetDidLoad(): void
    {
        throw new Error("Method not implemented.");
    }
    protected htmlTemplate(): string
    {
        throw new Error("Method not implemented.");
    }
    public setCustomPresenter(renderer: ICustomWidgetPresenter<Widget>): void
    {
        throw new Error("Method not implemented.");
    }
    public value(): string
    {
        throw new Error("Method not implemented.");
    }
    public setEnabled(enabled: boolean): void
    {
        throw new Error("Method not implemented.");
    }
    public addCSSClass(className: string): void
    {
        throw new Error("Method not implemented.");
    }
    public removeCSSClass(className: string): void
    {
        throw new Error("Method not implemented.");
    }
    public applyCSS(propertyName: string, propertyValue: string): void
    {
        throw new Error("Method not implemented.");
    }
    public setPosition(position: string, marginLeft: string, marginTop: string, marginRight: string, marginBottom: string, transform?: string): void
    {
        throw new Error("Method not implemented.");
    }
    public setVisible(visible: boolean): void
    {
        throw new Error("Method not implemented.");
    }

}
export class Mask
{
    /** 00/00/0000 */
    public static DATE: string = '00/00/0000';

    /**00:00:00 */
    public static TIME: string = '00:00:00';

    /**00/00/0000 00:00:00 */
    public static DATE_TIME: string = '00/00/0000 00:00:00';

    /**00000-000 */
    public static CEP: string = '00000-000';

    /**0000-0000 */
    public static PHONE: string = '0000-0000';

    /** (00) 0000-0000*/
    public static PHONE_DDD: string = '(00) 0000-0000';

    /**(000) 000-0000 */
    public static PHONE_US: string = '(000) 000-0000';

    /**000.000.000-00 */
    public static CPF: string = '000.000.000-00';

    /**00.000.000/0000-00 */
    public static CNPJ: string = '00.000.000/0000-00';

    /**000.000.000.000.000,00 */
    public static MONEY: string = '000.000.000.000.000,00';

    /**#.##0,00 */
    public static MONEY2: string = '#.##0,00';

    /**099.099.099.099 */
    public static IP_ADDRESS: string = '099.099.099.099';

    /**##0,00% */
    public static PERCENT: string = '##0,00%';

    public static array(): Array<string>
    {
        return [
            this.DATE,
            this.TIME,
            this.DATE_TIME,
            this.CEP,
            this.PHONE,
            this.PHONE_DDD,
            this.PHONE_US,
            this.CPF,
            this.CNPJ,
            this.MONEY,
            this.MONEY2,
            this.IP_ADDRESS,
            this.PERCENT
        ];
    }
}

export class UITextBoxBinder extends WidgetBinder
{
    private textBox: UITextBox;
    constructor(textBox: UITextBox) 
    {
        super(textBox);
        this.textBox = this.widget as UITextBox;
    }
    refreshUI(): void
    {
        var value = this.getModelPropertyValue();
        this.textBox.setText(`${value}`);
    }
    fillPropertyModel(): void
    {
        this.setModelPropertyValue(this.textBox.value());
    }
    getWidgetValue()
    {
        return this.textBox.value();
    }
}

export class UITextBox extends Widget implements IBindable
{

    public static toUpperCaseDefault: boolean = false;

    toUpperCase: boolean;
    floatPlaces: number;
    protected htmlTemplate(): string
    {
        return `
<div id="divContainer" class="${this.containerClass}">
    <label id="entryTitle" style="margin: 0px; padding: 0px; font-weight:normal !important;" for="inputEntry"> Entry Title </label>
    <div class="input-group">
        <span id="entrySymbol" style="height: 31px" class="input-group-text" >R$</span>
        <input id="entryInput" ${this.toUpperCase ? 'oninput="this.value = this.value.toUpperCase();"' : ''} class="form-control form-control-sm"  placeholder="Entry placeholder">
    </div>
</div>`
    }

    onWidgetDidLoad(): void
    {
        this.lbTitle = this.elementById('entryTitle');
        this.txInput = this.elementById('entryInput');
        this.divContainer = this.elementById('divContainer');
        this.spanSymbol = this.elementById('entrySymbol');

        if (Misc.isNullOrEmpty(this.initialSymbol))
            this.spanSymbol.remove()
        else
            this.spanSymbol.textContent = this.initialSymbol

        if (Misc.isNullOrEmpty(this.initialTitle))
            this.lbTitle.remove()
        else
            this.lbTitle.innerText = this.initialTitle;

        if (this.isFloat)
            this.txInput.inputMode = 'numeric';

        this.txInput.placeholder = this.initialPlaceHolder;

        this.setMaxLength(this.initialMaxlength);
        this.setInputType(this.initialType);
        this.applyMask(this.initialMask);

        if (this.required)
            this.txInput.setAttribute('required', 'required');

        this.setText(this.initialText)
    }


    public setEnabled(enabled: boolean): void
    {
        this.txInput.disabled = (enabled == false);
    }
    public setSymbol(symbol: string)
    {
        if (Misc.isNull(this.spanSymbol)) return;
        this.spanSymbol.textContent = symbol;
    }


    private initialTitle: string = null;
    private initialPlaceHolder: string = null;
    private initialText: string = null;
    private initialType: string = null;
    private initialMaxlength: number = null;
    private initialMask: string = null;
    private containerClass: string = null;
    private required: boolean = false;
    private initialSymbol: string = null

    public lbTitle: HTMLLabelElement = null;
    public txInput: HTMLInputElement = null;
    public divContainer: HTMLDivElement = null;
    public spanSymbol: HTMLSpanElement = null;

    constructor({
        name,
        type = 'text',
        title = '',
        maxlength = 100,
        placeHolder = '',
        text = '',
        mask = '',
        containerClass = 'form-group',
        isRequired = false,
        isFloat = false,
        floatPlaces = 2,
        symbol = '',
        toUpperCase = null
    }: {
        name: string;
        type?: string;
        mask?: string,
        maxlength?: number,
        title?: string;
        placeHolder?: string;
        text?: string;
        containerClass?: string
        isRequired?: boolean
        isFloat?: boolean,
        floatPlaces?: number
        symbol?: string,
        toUpperCase?: boolean
    })
    {
        super(name);

        this.isFloat = isFloat;
        this.required = isRequired;
        this.initialType = (Misc.isNullOrEmpty(type) ? 'text' : type);
        this.initialTitle = (Misc.isNullOrEmpty(title) ? '' : title);
        this.initialPlaceHolder = (Misc.isNullOrEmpty(placeHolder) ? '' : placeHolder);
        this.initialText = (Misc.isNullOrEmpty(text) ? '' : text);
        this.initialMaxlength = (Misc.isNullOrEmpty(maxlength) ? 100 : maxlength);
        this.initialMask = (Misc.isNull(mask) ? '' : mask);
        this.containerClass = (Misc.isNull(containerClass) ? 'form-group' : containerClass);
        this.initialSymbol = symbol
        this.floatPlaces = (Misc.isNull(floatPlaces) ? 2 : floatPlaces)

        if (type == 'email') toUpperCase = false

        if (!Misc.isNull(toUpperCase))
            this.toUpperCase = toUpperCase
        else
            this.toUpperCase = UITextBox.toUpperCaseDefault
    }
    public setOnEnter(fnOnEnter: Function, useOnBlurIfAppleMobile: boolean = false)
    {
        if (UIPage.isAppleMobileDevice() && useOnBlurIfAppleMobile)
        {
            this.txInput.enterKeyHint = 'done';
            this.txInput.onchange = (ev) => fnOnEnter();
            return
        }
        // dispositivo não-iOS
        this.txInput.onkeydown = (ev) => 
        {
            if (ev.key == 'Enter')
                fnOnEnter();
        }
    }

    /**
     * 
     * @param keyHandlers {[key: string]: Function}
      ```
       // example
       myTextBox.setOnKeyDown({
            'Enter': () => { },
            'F': () => { },
            'ArrowUp': () => { }
        })
        ```
     */
    public setOnKeyDown(keyHandlers: { [key: string]: Function })
    {
        this.txInput.enterKeyHint = 'done';
        this.txInput.onkeydown = (ev) => 
        {
            const handlers = keyHandlers[ev.key]
            if (!Misc.isNull(handlers))
                keyHandlers[ev.key]()
        }
    }

    getBinder(): WidgetBinder
    {
        return new UITextBoxBinder(this);
    }

    applyMask(maskPattern: string): void
    {
        if (Misc.isNullOrEmpty(maskPattern)) return;
        //making jQuery call
        var jQueryCall = `$('#${this.txInput.id}').mask('${maskPattern}'`;
        var a = Mask.array();
        var hasReverseFields = (
            (a.indexOf(Mask.CPF) +
                a.indexOf(Mask.CNPJ) +
                a.indexOf(Mask.MONEY) +
                a.indexOf(Mask.MONEY2)) >= 0)
        if (hasReverseFields)
            jQueryCall += ', {reverse: true});';
        else
            jQueryCall += ');';
        jQueryCall = `try { ${jQueryCall} } catch { }`;

        var maskFunction = new VirtualFunction({
            fnName: 'enableMask',
            fnContent: jQueryCall
        });
        maskFunction.call();
    }

    public setCustomPresenter(renderer: ICustomWidgetPresenter<UITextBox>): void
    {
        renderer.render(this);
    }

    public setInputType(inputType: string): void
    {
        this.txInput.type = inputType;
    }

    public focusAndSelect()
    {
        this.txInput.focus()
        this.txInput.select()
    }

    public focus()
    {
        this.txInput.focus();
    }

    public setMaxLength(maxlength: number): void
    {
        this.txInput.maxLength = maxlength;
    }

    public removeLabel()
    {
        this.lbTitle.remove();
    }
    public setPlaceholder(text: string): void
    {
        this.txInput.placeholder = text;
    }

    public getText(): string
    {
        return this.value();
    }
    private isFloat = false;
    public setText(newText: string): void
    {
        const tp = this.txInput.type;

        if (tp == 'color')
            this.txInput.value = newText
        if (tp == 'date')
            this.txInput.valueAsDate = (Misc.isNullOrEmpty(newText) ? new Date() : new Date(newText));
        if (tp == 'number' || this.isFloat)
        {
            if (newText.toString().indexOf('.') == -1 && newText.toString().indexOf(',') == -1)
            {
                const val = (Misc.isNullOrEmpty(newText) ? 0 : Number.parseInt(newText.toString()));
                if (this.txInput.type == 'number') this.txInput.valueAsNumber = val;
                else this.txInput.value = val.toString();
                return
            }
            else
            {
                const val = (Misc.isNullOrEmpty(newText) ? 0 : Number.parseFloat(newText.toString().replace(',', '.')))
                if (this.txInput.type == 'number') this.txInput.valueAsNumber = val;
                else this.txInput.value = `${val.toFixed(this.floatPlaces)}`
                this.isFloat = true;
                return
            }
        }
        if (tp == 'text')
            this.txInput.value = (Misc.isNullOrEmpty(newText) ? '' : newText);
    }

    public setTitle(newTitle: string): void
    {
        this.lbTitle.textContent = newTitle;
    }

    public value(): object | any | string | number
    {

        if (this.txInput.type == 'number' || this.isFloat)
        {
            var val = this.txInput.value
            if (val.indexOf('.') > -1 && val.indexOf(',') > -1)
            {
                val = val.replace('.', '')
                val = val.replace(',', '.')
            }
            else val = val.replace(',', '.')

            if (this.isFloat) return Number.parseFloat(val).toFixed(this.floatPlaces);
            else return Number.parseInt(this.txInput.value);
        }
        if (this.txInput.type == 'date')
            return new Date(this.txInput.value)

        if (this.txInput.type == 'text') return this.txInput.value.toString();
        return this.txInput.value;
    }

    public addCSSClass(className: string): void 
    {
        this.txInput.classList.add(className);
    }

    public removeCSSClass(className: string): void
    {
        this.txInput.classList.remove(className);
    }

    public applyCSS(propertyName: string, propertyValue: string): void 
    {
        this.txInput.style.setProperty(propertyName, propertyValue);
    }

    public setPosition(position: string,
        marginLeft: string,
        marginTop: string,
        marginRight: string,
        marginBottom: string,
        transform?: string): void 
    {
        this.divContainer.style.position = position;
        this.divContainer.style.left = marginLeft;
        this.divContainer.style.top = marginTop;
        this.divContainer.style.right = marginRight;
        this.divContainer.style.bottom = marginBottom;
        this.divContainer.style.transform = transform;
    }

    public setVisible(visible: boolean): void 
    {
        this.divContainer.style.visibility = (visible ? 'visible' : 'hidden')
    }

}
export class UITextAreaBinder extends WidgetBinder
{
    private textBox: UITextArea;
    constructor(textBox: UITextArea) 
    {
        super(textBox);
        this.textBox = this.widget as UITextArea;
    }
    refreshUI(): void
    {
        var value = this.getModelPropertyValue();
        this.textBox.setText(`${value}`);
    }
    fillPropertyModel(): void
    {
        var text: string = this.textBox.getText();
        this.setModelPropertyValue(text);
    }
    getWidgetValue()
    {
        var text: string = this.textBox.getText();
        return text;
    }
}

export class UITextArea extends Widget implements IBindable
{

    public static toUpperCaseDefault: boolean = false;
    toUpperCase: boolean;
    protected htmlTemplate(): string
    {
        return `
<div id="divContainer" class="form-group">
    <label id="entryTitle" style="margin: 0px; padding: 0px; font-weight:normal !important;" for="inputEntry"> Entry Title </label>
    <textarea id="entryInput" ${this.toUpperCase ? 'oninput="this.value = this.value.toUpperCase();"' : ''} class="form-control form-control-sm"> </textarea>
</div>`
    }
    public setEnabled(enabled: boolean): void
    {
        this.txInput.disabled = (enabled == false);
    }


    private initialTitle: string = null;
    private initialText: string = null;
    private initialMaxlength: number = null;
    private initialHeight: string = null;

    public lbTitle: HTMLLabelElement = null;
    public txInput: HTMLTextAreaElement = null;
    public divContainer: HTMLDivElement = null;

    constructor({ name, title = '', height = '100px', maxlength = 255, text = '', toUpperCase = null }:
        {
            name: string
            title?: string
            height?: string
            maxlength?: number
            text?: string,
            toUpperCase?: boolean
        })
    {
        super(name);

        this.initialHeight = (Misc.isNull(height) ? '100px' : height);
        this.initialTitle = (Misc.isNullOrEmpty(title) ? '' : title);
        this.initialText = (Misc.isNullOrEmpty(text) ? '' : text);
        this.initialMaxlength = (Misc.isNullOrEmpty(maxlength) ? 100 : maxlength);
        if (!Misc.isNull(toUpperCase))
            this.toUpperCase = toUpperCase
        else
            this.toUpperCase = UITextArea.toUpperCaseDefault
    }
    getBinder(): WidgetBinder
    {
        return new UITextAreaBinder(this);
    }

    public setCustomPresenter(renderer: ICustomWidgetPresenter<UITextArea>): void
    {
        renderer.render(this);
    }

    onWidgetDidLoad(): void
    {
        this.lbTitle = this.elementById<HTMLLabelElement>('entryTitle');
        this.txInput = this.elementById<HTMLTextAreaElement>('entryInput');
        this.divContainer = this.elementById<HTMLDivElement>('divContainer');

        this.lbTitle.innerText = this.initialTitle;
        this.txInput.value = this.initialText;
        this.txInput.style.height = this.initialHeight;

        this.setMaxLength(this.initialMaxlength);
    }

    public setMaxLength(maxlength: number): void
    {
        this.txInput.maxLength = maxlength;
    }

    public removeLabel()
    {
        this.lbTitle.remove();
    }
    public setPlaceholder(text: string): void
    {
        this.txInput.placeholder = text;
    }

    public getText(): string
    {
        return this.value();
    }

    public setText(newText: string): void
    {
        this.txInput.value = (Misc.isNullOrEmpty(newText) ? '' : newText);
    }

    public setTitle(newTitle: string): void
    {
        this.lbTitle.textContent = newTitle;
    }

    public value(): string
    {
        return this.txInput.value;
    }

    public addCSSClass(className: string): void 
    {
        this.txInput.classList.add(className);
    }

    public removeCSSClass(className: string): void
    {
        this.txInput.classList.remove(className);
    }

    public applyCSS(propertyName: string, propertyValue: string): void 
    {
        this.txInput.style.setProperty(propertyName, propertyValue);
    }

    public setPosition(position: string,
        marginLeft: string,
        marginTop: string,
        marginRight: string,
        marginBottom: string,
        transform?: string): void 
    {
        this.divContainer.style.position = position;
        this.divContainer.style.left = marginLeft;
        this.divContainer.style.top = marginTop;
        this.divContainer.style.right = marginRight;
        this.divContainer.style.bottom = marginBottom;
        this.divContainer.style.transform = transform;
    }

    public setVisible(visible: boolean): void 
    {
        this.divContainer.style.visibility = (visible ? 'visible' : 'hidden')
    }

}
export class UIToast extends Widget
{
    text: string;

    private constructor(name: string, text: string)
    {
        super(name);
        this.text = text;
    }

    toastDiv: HTMLDivElement;

    protected htmlTemplate(): string
    {
        return `
        <div class="d-flex justify-content-center">
            <style>
                .toast-success{
                    background: rgb(221, 255, 221);
                    border: solid 1px rgb(0, 94, 0);
                }
                .toast-error{
                    background: rgb(255, 188, 188);
                    border: solid 1px rgb(255, 79, 79);
                }
                .dv-toast {
                    opacity: 0;
                    position: absolute;
                    min-width: 100px;
                    max-width: 500px;
                    min-height: 40px;
                    margin-top: 10px;
                    z-index: 10;
                    border-radius: 8px;
         
                }
            </style>
            <div id="toast-div" class="dv-toast ${this.widgetName} ps-2 pe-2 shadow-lg d-flex justify-content-center align-items-center text-center">
                <!-- toast text -->
            </div>
        </div>
    `
    }

    protected onWidgetDidLoad(): void
    {
        this.toastDiv = this.elementById('toast-div')
        this.toastDiv.textContent = this.text
        const $ = this;
        this.toastDiv.onclick = () => $.toastDiv.remove()
    }

    public static success(text: string, targetDivId: string)
    {
        const toast = new UIToast('toast-success', text)
        UIToast.show(targetDivId, toast);
    }


    public static error(text: string, targetDivId: string)
    {
        const toast = new UIToast('toast-error', text)
        UIToast.show(targetDivId, toast);
    }

    private static show(targetDivId: string, toast: UIToast)
    {
        const ctx = new WidgetContext(UIPage.shell, [targetDivId]);
        ctx.addWidget(targetDivId, toast);
        ctx.build(new class noty implements INotifiable
        {
            onNotified(sender: any, args: any[]): void
            {
                var is = setInterval(() =>
                {
                    if (toast.toastDiv.style.opacity == '')
                        toast.toastDiv.style.opacity = '0'

                    if (parseFloat(toast.toastDiv.style.opacity) < 1.0)
                    {
                        toast.toastDiv.style.opacity = `${parseFloat(toast.toastDiv.style.opacity) + 0.08}`;
                    }

                    else
                    {
                        clearInterval(is);
                        var t = setTimeout(() =>
                        {

                            var it = setInterval(() =>
                            {
                                if (parseFloat(toast.toastDiv.style.opacity) > 0)
                                {
                                    toast.toastDiv.style.opacity = `${parseFloat(toast.toastDiv.style.opacity) - 0.02}`;
                                }
                                else
                                {
                                    toast.toastDiv.remove();
                                    ctx.clear();
                                    clearInterval(it);
                                }
                            }, 50);

                            clearTimeout(t);
                        }, 1500);
                    }

                }, 50);
            }

        });
    }
}
export class DataGridItem implements IDataGridItemTemplate
{
    public value: any;
    public itemName: string;
    public ownerDatagrid: UIDataGrid;
    public rowElement: HTMLTableRowElement;
    private pageShell: PageShell;

    private selected: boolean = false;

    constructor(name: string,
        model: any | object,
        pageShell: PageShell)
    {
        this.pageShell = pageShell;
        this.itemName = name;
        this.value = model;
    }

    setOwnerDataGrid(dataGrid: UIDataGrid): void
    {
        this.ownerDatagrid = dataGrid;
    }
    isSelected(): boolean
    {
        return this.selected;
    }
    select(): void
    {
        this.selected = true;
        this.rowElement.style.background = this.ownerDatagrid.selectedBackColor;
        this.rowElement.style.color = this.ownerDatagrid.selectedForeColor;
    }
    unSelect(): void
    {
        this.selected = false;
        this.rowElement.style.background = this.ownerDatagrid.unselectedBackColor;
        this.rowElement.style.color = this.ownerDatagrid.unselectedForeColor;
    }
    itemTemplate(): HTMLTableRowElement
    {
        var self = this;
        if (self.rowElement != null)
            return self.rowElement;

        var model = self.value;
        var tr = self.pageShell.createElement('tr')
        
        for (var k = 0; k < this.ownerDatagrid.MODEL_KEYS.length; k++)
        {
            var key = this.ownerDatagrid.MODEL_KEYS[k];
            var td = this.pageShell.createElement('td');

            td.innerText = model[key];
            tr.appendChild(td);
        }
        tr.onclick = function (ev)
        {
            self.ownerDatagrid.onRowClick(self);
        };

        self.rowElement = tr;
        return tr;
    }
}
export class ToolbarState
{
    public readonly name: string;
    public readonly buttons: UIButton[];
    public readonly stateCallback: Function;

    public constructor(name: string, callback: Function, ...buttons: UIButton[])
    {
        this.name = name;
        this.buttons = buttons;
        this.stateCallback = callback;
    }

}

export class UIToolBar extends Widget
{
    public static $: UIToolBar;

    icon: string;
    tittle: string;

    public states: ToolbarState[];

    optionsContext: WidgetContext;

    constructor({ name, iconPath, barTittle }: {
        name: string;
        iconPath: string;
        barTittle: string;
    })
    {
        super(name);
        this.icon = iconPath;
        this.tittle = barTittle;
        this.states = [];
        UIToolBar.$ = this;
    }

    public defineState(name: string, callback: Function, ...buttons: UIButton[]): UIToolBar
    {
        this.states.push(new ToolbarState(name, callback, ...buttons));
        return this;
    }


    currentState: ToolbarState;

    public activateState(stateName: string)
    {
        this.currentState = null;
        for (var e = 0; e < this.states.length; e++)
            if (this.states[e].name == stateName)
            {
                this.currentState = this.states[e];
                break;
            }
        if (Misc.isNull(this.currentState))
            throw new Error(`The stete '${stateName}' does not registered in toolBar ${this.widgetName}`);

        this.optionsContext.clear();
        var stateButtons = this.currentState.buttons;
        for (var b = 0; b < stateButtons.length; b++)
            this.optionsContext.addWidget(this.containerId, stateButtons[b]);

        const $ = this;
        this.optionsContext.build(new class Noty implements INotifiable
        {
            onNotified(sender: any, args: any[]): void
            {
                var botoes = $.currentState.buttons;
                for (var w = 0; w < botoes.length; w++)
                {
                    const btn = botoes[w];
                    btn.setOnClickFn(() => UIToolBar.$.currentState.stateCallback(btn.widgetName));
                }
            }
        }, false);
    }

    protected htmlTemplate(): string
    {
        return `
<div class="d-flex flex-row card shadow-sm mx-2 p-0 mt-2" style="height:52px; background:whitesmoke">

    <div style="width:55px; height:48px; padding: 3px">
        <img style="width:45px; height:43px; padding:5px" class="card shadow-sm" id="ic_rotina" />
    </div>

    <label id="lb_titulo" class="h5 col-md-5 col-xs-2 mt-2 ps-0"> Nome Rotina </label>

    <div id="dv_opcoes" class="col d-flex justify-content-end mt-2 mb-2 me-2" style="overflow-x:auto">
      
    </div>

</div>
       
       `;
    }

    containerId: string;
    protected onWidgetDidLoad(): void
    {
        const iconeEl = this.elementById<HTMLImageElement>('ic_rotina');
        const tituloEl = this.elementById<HTMLLabelElement>('lb_titulo');
        const dvOpcoes = this.elementById<HTMLDivElement>('dv_opcoes');

        this.containerId = dvOpcoes.id;
        iconeEl.src = this.icon;
        tituloEl.textContent = this.tittle;
        this.optionsContext = new WidgetContext(this.getPageShell(), [this.containerId])
    }

    public getOption(buttonName: string): UIButton
    {
        return this.optionsContext.get<UIButton>(`${this.containerId}/${buttonName}`)
    }
}
export interface IDataGridItemTemplateProvider
{
    getDataGridItemTemplate(sender: UIDataGrid, viewModel: any | object): IDataGridItemTemplate;
}
export interface IDataGridItemTemplate
{
    value: any | object;
    itemName: string;
    setOwnerDataGrid(dataGrid: UIDataGrid): void;
    isSelected(): boolean;
    select(): void;
    unSelect(): void;
    itemTemplate(): HTMLTableRowElement;
}
export interface IListItemTemplateProvider
{
    getListItemTemplate(sender: UIList, viewModel: any|object): IListItemTemplate;
}
export interface IListItemTemplate 
{
    value: any|object;
    itemName: string;
    setOwnerList(listView: UIList): void;
    isSelected(): boolean;
    select(): void;
    unSelect(): void;
    itemTemplate(): HTMLAnchorElement;
}
export class ListItem implements IListItemTemplate
{
    public viewModel: any | object;
    public value: any | object;
    public itemName: string;
    public itemText: string;
    public itemImageSource: string;
    public itemBadgeText: string;
    public ownerList: UIList;
    public anchorElement: HTMLAnchorElement;
    public imgElement: HTMLImageElement;
    public divElement: HTMLDivElement;
    public badgeElement: HTMLSpanElement;


    private selected: boolean = false;


    constructor(name: string,
        text: string,
        value?: any | object,
        imageSrc: string = null,
        badgeText: string = null)
    {
        this.value = value;
        this.itemName = name;
        this.itemText = text;
        this.itemImageSource = imageSrc;
        this.itemBadgeText = badgeText;
    }

    public setImg(src: string): void
    {
        if (Misc.isNullOrEmpty(src))
        {
            this.imgElement.hidden = true;
            this.imgElement.width = 0;
        }
        else
        {
            if (this.imgElement.hidden == true) this.imgElement.hidden = false;
            if (this.imgElement.width == 0) this.imgElement.width = 30;
            this.imgElement.src = src;
        }
    }

    public setText(text: string): void
    {
        this.divElement.textContent = text;
    }

    public setBadgeText(badgeText: string): void
    {
        this.badgeElement.textContent = badgeText;
    }

    public setOwnerList(listView: UIList)
    {
        this.ownerList = listView;
    }

    public isSelected(): boolean
    {
        return this.selected;
    }

    public select(): void
    {
        this.selected = true;

        if (this.ownerList.customBehaviorColors)
        {
            this.anchorElement.style.color = this.ownerList.selectedForeColor;
            this.anchorElement.style.backgroundColor = this.ownerList.selectedBackColor;
        }
        else
            this.anchorElement.classList.add('active');
    }

    public unSelect(): void
    {
        this.selected = false;
        if (this.ownerList.customBehaviorColors)
        {
            this.anchorElement.style.color = this.ownerList.unSelectedForeColor;
            this.anchorElement.style.backgroundColor = this.ownerList.unSelectedBackColor;
        }
        else
            this.anchorElement.classList.remove('active');
    }

    public itemTemplate(): HTMLAnchorElement
    {
        var self = this;
        if (self.anchorElement != null)
            return self.anchorElement;

        var pageShell = self.ownerList.getPageShell();

        self.anchorElement = pageShell.createElement('a');
        self.anchorElement.style.padding = '0px';
        self.anchorElement.classList.add('list-group-item', 'align-items-center', 'list-group-item-action');
        self.anchorElement.id = this.itemName;

        var rowDiv = pageShell.createElement('div');
        rowDiv.style.background = 'transparent';
        rowDiv.style.height = '40px';
        rowDiv.style.marginTop = '10px'
        rowDiv.classList.add('row');

        var col10Div = pageShell.createElement('div');
        col10Div.style.paddingLeft = '25px';
        col10Div.classList.add('col-10');

        var img: HTMLImageElement = null;
        if (this.itemImageSource != null)
        {
            img = pageShell.createElement('img');
            img.src = this.itemImageSource;
            img.style.marginRight = '10px';
            img.width = 30;
            img.height = 30;

            col10Div.append(img);
        }

        col10Div.append(this.itemText);

        rowDiv.append(col10Div);

        var badgeSpan: HTMLSpanElement = null;
        if (this.itemBadgeText != null)
        {
            var col2Div = pageShell.createElement('div');
            col2Div.style.display = 'flex'
            col2Div.style.justifyContent = 'end'
            col2Div.style.alignSelf = 'center'

            col2Div.classList.add('col-2');
            badgeSpan = pageShell.createElement('span');

            badgeSpan.classList.add('badge', 'badge-success', 'badge-pill');
            badgeSpan.textContent = this.itemBadgeText;
            badgeSpan.style.marginRight = '10px'

            col2Div.append(badgeSpan);
            rowDiv.append(col2Div);
        }

        self.anchorElement.append(rowDiv);
        self.badgeElement = badgeSpan;
        self.imgElement = img;
        self.divElement = rowDiv;

        this.unSelect();
        return self.anchorElement;
    }
}
export class UITemplateView 
{
    public templateDOM: Document;
    public templateString: string;

    private viewDictionary: Array<ViewDictionaryEntry>;
    private shellPage: PageShell;

    /**
     * 
     * @param htmlContent 
     * @param shell 
     * @param data 
     */
    constructor(htmlContent: string, shell: PageShell, data?: any | object)
    {
        this.shellPage = shell;
        this.viewDictionary = [];
        //  this.parentFragment.clear();

        var html: string = htmlContent.trim();

        if (!Misc.isNull(data))
        {
            for (var prop in data)
            {
                var find = `#${prop}`;
                while (html.indexOf(find) != -1)
                    html = html.replace(find, data[prop])
            }
        }

        var parser = new DOMParser();
        var domObj = parser.parseFromString(html, "text/html");
        var allIds = domObj.querySelectorAll('*[id]');

        for (var i = 0; i < allIds.length; i++)
        {
            var element = allIds[i];
            var currentId = element.getAttribute('id');
            if (currentId != null)
            {
                var newId = `${currentId}_${Widget.generateUUID()}`;
                this.addDictionaryEntry(currentId, newId);
                element.setAttribute('id', newId);
            }
        }

        this.templateDOM = domObj;
        this.templateString = domObj.children[0].outerHTML;
    }

    public content(): Element
    {
        var body = this.templateDOM.children[0].children[1];
        if (body.innerHTML.indexOf('<') == -1)
        {
            var span = document.createElement('span') as HTMLSpanElement;
            span.textContent = body.innerHTML;
            return span
        }
        else
            return body.children[0];
    }

    public elementById<TElement>(elementId: string): TElement
    {
        for (var i = 0; i < this.viewDictionary.length; i++)
        {
            var entry: ViewDictionaryEntry = this.viewDictionary[i];
            if (entry.getOriginalId() == elementId)
            {
                var elementResult: any = this.templateDOM.getElementById(entry.getManagedId());
                if (Misc.isNull(elementResult))
                    elementResult = document.getElementById(entry.getManagedId())
                return elementResult;
            }
        }
        return null as unknown as TElement;
    }

    private addDictionaryEntry(originalId: string, generatedId: string)
    {
        var entry = new ViewDictionaryEntry(originalId, generatedId);
        this.viewDictionary.push(entry);
    }
}
export class DataGridColumnDefinition
{
    /**Column Header */
    public h: string;

    /**Model key (property) name */
    public k: string;
}

export class UIDataGridBinder extends WidgetBinder
{
    private dataGrid: UIDataGrid;
    constructor(dataGrid: UIDataGrid)
    {
        super(dataGrid);
        this.dataGrid = dataGrid;
    }

    getWidgetValue()
    {
        return this.dataGrid.selectedValue();
    }
    refreshUI(): void
    {
        var viewModels: Array<any | object> = this.getModelPropertyValue();
        this.dataGrid.fromList(viewModels);

        if (this.isTargetDefined())
        {
            var value = this.getModelTargetPropertyValue();
            this.dataGrid.setSelectedValue(value);
        }
    }
    fillPropertyModel(): void
    {
        if (this.isTargetDefined())
        {
            this.fillModelTargetPropertyValue();
        }
    }
}

export class UIDataGrid extends Widget implements IBindable
{
    public autoGenerateColumns: boolean;
    public table: HTMLTableElement;
    public tableHeader: HTMLTableSectionElement;
    public tableBody: HTMLTableSectionElement;
    public selectedBackColor: string = '#007BFF';
    public unselectedBackColor: string = '#FFFFFF';

    public selectedForeColor: string = '#FFFFFF'
    public unselectedForeColor: string = '#000000'

    private templateProvider: IDataGridItemTemplateProvider;
    private items: Array<IDataGridItemTemplate> = [];

    public MODEL_KEYS: Array<string> = [];

    private baseTemplate: string = null;


    private tableCssClasses: string = 'table table-hover table-bordered table-sm';
    constructor({ name, autoGenCols = false, baseTemplate = null, cssClasses = null }: {
        name: string,
        autoGenCols?: boolean,
        itemTemplateProvider?: IDataGridItemTemplateProvider,
        baseTemplate?: string,
        cssClasses?: string
    })
    {
        super(name);
        this.autoGenerateColumns = autoGenCols;
        this.baseTemplate = baseTemplate;
        if (!Misc.isNullOrEmpty(cssClasses))
            this.tableCssClasses = cssClasses;
    }


    protected htmlTemplate(): string
    {
        if (!Misc.isNullOrEmpty(this.baseTemplate))
        {
            if (this.baseTemplate.indexOf('gridHeader') == -1)
                throw new Error(`UIDataGrid '${this.widgetName}' failed to load: custom base-template does not contains an <div/> with Id="gridHeader".`)
            if (this.baseTemplate.indexOf('gridBody') == -1)
                throw new Error(`UIDataGrid '${this.widgetName}' failed to load: custom base-template does not contains an <div/> with Id="gridBody".`)

            return this.baseTemplate;
        }
        return `
<table id="fsDataGrid" class="${this.tableCssClasses}">
  <thead id="gridHeader">
  </thead>
  <tbody id="gridBody" style="overflow-y:scroll;">
  </tbody>
</table>        
`;
    }

    protected onWidgetDidLoad(): void
    {
        this.table = this.elementById('fsDataGrid');
        this.table.style.background = 'white';
        this.tableHeader = this.elementById('gridHeader');
        this.tableBody = this.elementById('gridBody');
    }


    getBinder(): WidgetBinder
    {
        return new UIDataGridBinder(this);
    }

    /**
     * 
     * @param colDefs array of { h: 'Column Header', k: 'model_property_name' }
     */
    public addColumns(colDefs: Array<DataGridColumnDefinition>): void
    {
        this.table.tHead.innerHTML = '';
        for (var i = 0; i < colDefs.length; i++)
        {
            var def: DataGridColumnDefinition = colDefs[i];
            this.addColumn(def.h, def.k);
        }
    }

    public setTemplateProvider(provider: IDataGridItemTemplateProvider): UIDataGrid
    {
        this.templateProvider = provider;
        return this;
    }

    public addColumn(columnHeader: string, modelKey: string): UIDataGrid
    {
        var shell = this.getPageShell();
        this.MODEL_KEYS.push(modelKey);
        var thead = this.table.tHead;

        if (thead.childNodes.length == 0)
            thead.appendChild(shell.createElement('tr'));

        var th = shell.createElement('th', columnHeader);
        th.scope = 'col';
        thead.children[0].appendChild(th);

        return this;
    }

    private generateColumns(list: Array<any>): void
    {
        this.autoGenerateColumns = false;
        this.table.tHead.innerHTML = '';
        this.MODEL_KEYS = [];

        var shell = this.getPageShell();

        //creating columns
        var tr: HTMLTableRowElement = shell.createElement('tr');
        let firstModel = list[0];
        for (let key in firstModel)
        {
            var th = shell.createElement('th');
            th.scope = 'col';
            th.textContent = key;
            tr.appendChild(th);
            this.MODEL_KEYS.push(key);
        }
        this.table.tHead.appendChild(tr);
    }

    public fromList(list: Array<any>): void
    {
        this.table.tBodies[0].innerHTML = '';
        this.items = [];

        if ((list == null || list == undefined) || list.length == 0)
            return;

        var shell = this.getPageShell();
        if (this.autoGenerateColumns)
            this.generateColumns(list);

        //adding rows
        for (var i = 0; i < list.length; i++)
        {
            var model = list[i];

            var itemTemplate: IDataGridItemTemplate;
            if (this.templateProvider == null)
                itemTemplate = new DataGridItem(`default_datagrid_item_${i + 1}`, model, shell);
            else
                itemTemplate = this.templateProvider.getDataGridItemTemplate(this, model);

            itemTemplate.setOwnerDataGrid(this);
            this.items.push(itemTemplate);
            this.table.tBodies[0].appendChild(itemTemplate.itemTemplate());
        }
    }

    public selectedItem(): IDataGridItemTemplate
    {
        for (var i = 0; i < this.items.length; i++)
            if (this.items[i].isSelected())
                return this.items[i];
        return null;
    }

    public selectedValue(): any | object
    {
        for (var i = 0; i < this.items.length; i++)
            if (this.items[i].isSelected())
                return this.items[i].value;
        return null;
    }

    public setSelectedItem(item: IDataGridItemTemplate): void
    {
        for (var i = 0; i < this.items.length; i++)
            this.items[i].unSelect();
        item.select();
    }

    public setSelectedValue(model: any | object): void
    {
        for (var i = 0; i < this.items.length; i++)
        {
            var item = this.items[i];
            if (item.value == model)
                item.select();
            else
                item.unSelect();
        }
    }

    public onRowClick(item: IDataGridItemTemplate): void
    {
        for (var i = 0; i < this.items.length; i++)
            this.items[i].unSelect();
        item.select();
    }

    public setCustomPresenter(presenter: ICustomWidgetPresenter<Widget>): void
    {
        presenter.render(this);
    }
    public value(): string
    {
        return this.selectedValue();
    }

    public applyCSS(propertyName: string, propertyValue: string): void
    {
        this.table.style.setProperty(propertyName, propertyValue);
    }

    public setVisible(visible: boolean): void
    {
        this.table.style.visibility = (visible ? 'visible' : 'hidden')
    }

}
export class DivContent
{
    id: string = '';
    w: Widget[]

    constructor(id: string, ...w: Widget[])
    {
        this.id = id;
        this.w = w;
    }
}
export interface IYordLayout
{
    getLayout(): ViewLayout;
}
export class YordManagedView extends UIView
{
    private yordView: YordView;
    private yordCtx: YordViewContext;

    constructor(view: YordView, context: YordViewContext)
    {
        super();
        view.managedView = this;
        this.yordView = view;
        this.yordCtx = context;
    }

    buildLayout(): ViewLayout
    {
        return this.yordView.viewLayout.getLayout();
    }

    composeView(): void
    {
        if (Misc.isNull(this.yordView.viewComposing))
            return;

        for (var c = 0; c < this.yordView.viewComposing.length; c++)
        {
            var composingInfo = this.yordView.viewComposing[c];
            var widgets: Widget[] = composingInfo.w;
            this.addWidgets(composingInfo.id, ...widgets);
        }
    }

    onViewDidLoad(): void
    {
        this.yordView.onLoad(this.viewContext(), this.yordCtx);
    }
}
export abstract class YordView
{
    public readonly viewName: string;
    public viewLayout: IYordLayout;
    public viewWidgets: Widget[];
    public viewComposing: DivContent[];
    public managedView: YordManagedView;

    constructor(viewName: string)
    {
        this.viewName = viewName;
    }
    
    public useLayout(layout: IYordLayout): YordView
    {
        this.viewLayout = layout;
        return this;
    }

    public declareAndCompose(...composing: DivContent[]): YordView
    {
        this.viewComposing = composing;
        return this;
    }

    public createBinding<TModel>(model: TModel): BindingContext<TModel>
    {
        return new BindingContext<TModel>(model, this.managedView);
    }

    public abstract onInit(): void;
    public abstract onLoad(wc: WidgetContext, mvc: YordViewContext): void;

}
export class YordViewContext
{
    private managedViews: YordView[] = [];
    private pageShell: PageShell;

    constructor(shell: PageShell)
    {
        this.pageShell = shell;
    }

    public addView(view: YordView): YordViewContext
    {
        this.managedViews.push(view);
        return this;
    }

    public goTo(viewName: string): void
    {
        for (var v = 0; v < this.managedViews.length; v++)
        {
            var view: YordView = this.managedViews[v];
            if (view.viewName == viewName)
            {
                view.onInit();

                var managedView: YordManagedView = new YordManagedView(view, this);
                this.pageShell.navigateToView(managedView);
                break;
            }
        }
    }
}
export class HtmlLayout implements IYordLayout
{
    private htmlString: string = '';
    private containerDivId: string = '';
    constructor(containerDivId: string, rawHtmlString: string) {
        this.htmlString = rawHtmlString;
        this.containerDivId = containerDivId;
    }

    getLayout(): ViewLayout
    {
        return new ViewLayout(this.containerDivId)
            .fromHTML(this.htmlString);
    }
}
export class Language
{
      constructor(name: string)
      {
            this.name = name;
      }

      public name: string;
      public entries: Array<LanguageEntry> = [];

      public addEntry(key: string, value: string): void
      {
            this.entries.push(new LanguageEntry(key, value));
      }
}
export class LanguageEntry
{
      public key: string = '';
      public value: string = '';

      constructor(key: string, value: string)
      {
            this.key = key;
            this.value = value;
      }
}
export abstract class LanguageServer
{
      private languages: Array<Language> = []

      protected getLanguage(name: string): Language
      {
            for (var i = 0; i < this.languages.length; i++)
                  if (this.languages[i].name == name)
                        return this.languages[i]
            return null
      }

      protected add(key: string, langName: string, value: string): void
      {
            var lang = this.getLanguage(langName)
            if (Misc.isNull(lang))
            {
                  lang = new Language(langName);
                  this.languages.push(lang)
            }

            lang.addEntry(key, value)
      }

      public translate(key: string, langName: string): string
      {
            for (var i = 0; i < this.languages.length; i++)
                  if (this.languages[i].name == langName)
                        return this.languages[i].entries.find(x => x.key == key)?.value
            return '';
      }
}
