objective-ui
Version:
A typescript frontend oriented-object based framework
1,603 lines (1,386 loc) • 236 kB
text/typescript
/**
* 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 =