diff --git a/src/core/BaseApplication.ts b/src/core/BaseApplication.ts index 7af4bb7..c5dac45 100644 --- a/src/core/BaseApplication.ts +++ b/src/core/BaseApplication.ts @@ -144,6 +144,33 @@ namespace OS { }); } + /** + * API function to register responsive UI event to the current window tag + * + * @protected + * @param {GUI.TagResponsiveValidator} responsive validator + * @param {GUI.TagResponsiveCallback} responsive callback + * @returns {void} + * @memberof BaseApplication + */ + protected morphon(validator: GUI.TagResponsiveValidator, callback: GUI.TagResponsiveCallback) + { + const win = this.scheme as GUI.tag.WindowTag; + win.morphon(validator, callback); + } + /** + * API function to unregister responsive UI event from current window tag + * + * @protected + * @param {GUI.TagResponsiveValidator} responsive validator + * @returns {void} + * @memberof BaseApplication + */ + protected morphoff(validator: GUI.TagResponsiveValidator) + { + const win = this.scheme as GUI.tag.WindowTag; + win.morphoff(validator); + } /** * Render the application UI by first loading its scheme * and then mount this scheme to the DOM tree diff --git a/src/core/BaseModel.ts b/src/core/BaseModel.ts index 01c172f..2f9d402 100644 --- a/src/core/BaseModel.ts +++ b/src/core/BaseModel.ts @@ -701,12 +701,16 @@ namespace OS { * @returns {HTMLElement} * @memberof BaseModel */ - protected find(id: string): HTMLElement { + protected find(id: string): T { if (this.scheme) { - return $(`[data-id='${id}']`, this.scheme)[0]; + return $(`[data-id='${id}']`, this.scheme)[0] as T; } } + /*protected $(id: string) : T { + return this.find(id) as T; + }*/ + /** * Select all DOM Element inside the UI of the model * using JQuery selector diff --git a/src/core/tags/FloatListTag.ts b/src/core/tags/FloatListTag.ts index 388a406..cdb862f 100644 --- a/src/core/tags/FloatListTag.ts +++ b/src/core/tags/FloatListTag.ts @@ -101,6 +101,7 @@ namespace OS { * @memberof FloatListTag */ protected mount(): void { + $(this.refs.current).hide(); $(this.refs.container) .css("width", "100%") .css("height", "100%"); diff --git a/src/core/tags/GridViewTag.ts b/src/core/tags/GridViewTag.ts index eb32e16..c445fae 100644 --- a/src/core/tags/GridViewTag.ts +++ b/src/core/tags/GridViewTag.ts @@ -1248,6 +1248,7 @@ namespace OS { { el: "div", ref: "container", + class: "grid_content_container", children: [{ el: "div", ref: "grid" }], }, ]; diff --git a/src/core/tags/ListViewTag.ts b/src/core/tags/ListViewTag.ts index 86e8046..7b6547f 100644 --- a/src/core/tags/ListViewTag.ts +++ b/src/core/tags/ListViewTag.ts @@ -548,6 +548,9 @@ namespace OS { */ private _data: GenericObject[]; + private _drop: (any) => void; + private _show: (any) => void; + /** * Event data passing between mouse event when performing * drag and drop on the list @@ -577,6 +580,8 @@ namespace OS { ) => {}; this._selectedItems = []; this._selectedItem = undefined; + this._drop = (e) => {this.dropoff(e)}; + this._show = (e) => {this.showlist(e)}; } /** @@ -617,23 +622,18 @@ namespace OS { $(this.refs.container).removeAttr("style"); $(this.refs.mlist).removeAttr("style"); $(this).removeClass("dropdown"); - const drop = (e: any) => { - return this.dropoff(e); - }; - const show = (e: any) => { - return this.showlist(e); - }; if (v) { $(this).addClass("dropdown"); $(this.refs.current).show(); - $(document).on("click", drop); - $(this.refs.current).on("click", show); + $(document).on("click", this._drop); + $(this.refs.current).on("click", this._show); $(this.refs.mlist).hide(); this.calibrate(); } else { + $(document).off("click", this._drop); + $(this.refs.current).off("click", this._show); $(this.refs.current).hide(); - $(document).off("click", drop); - $(this.refs.current).off("click", show); + $(this.refs.mlist).show(); } } @@ -1157,10 +1157,11 @@ namespace OS { ); } } - + + // set the label content event it is hidden + const label = this.refs.drlabel as LabelTag; + label.set(e.data.data); if (this.dropdown) { - const label = this.refs.drlabel as LabelTag; - label.set(e.data.data); $(this.refs.mlist).hide(); } const evt = { id: this.aid, data: edata }; @@ -1350,23 +1351,37 @@ namespace OS { } /** - * Scroll the list view to bottom + * Scroll the list view to end * * @memberof ListViewTag */ - scroll_to_bottom() + scroll_to_end() { - this.refs.mlist.scrollTo({ top: this.refs.mlist.scrollHeight, behavior: 'smooth' }) + if(this.dir == "vertical") + { + this.refs.mlist.scrollTo({ top: this.refs.mlist.scrollHeight, behavior: 'smooth' }); + } + else + { + this.refs.mlist.scrollTo({ left: this.refs.mlist.scrollWidth, behavior: 'smooth' }); + } } /** - * Scroll the list view to top + * Scroll the list view to beginning * * @memberof ListViewTag */ - scroll_to_top() + scroll_to_start() { - this.refs.mlist.scrollTo({ top: 0, behavior: 'smooth' }); + if(this.dir == "vertical") + { + this.refs.mlist.scrollTo({ top: 0, behavior: 'smooth' }); + } + else + { + this.refs.mlist.scrollTo({ left: 0, behavior: 'smooth' }); + } } /** diff --git a/src/core/tags/OverlayTag.ts b/src/core/tags/OverlayTag.ts index 2029c0e..5096e5d 100644 --- a/src/core/tags/OverlayTag.ts +++ b/src/core/tags/OverlayTag.ts @@ -136,6 +136,7 @@ namespace OS { w: this.width, h: this.height, }, + root: true }); } diff --git a/src/core/tags/ResizerTag.ts b/src/core/tags/ResizerTag.ts index 4f82306..1af3239 100644 --- a/src/core/tags/ResizerTag.ts +++ b/src/core/tags/ResizerTag.ts @@ -187,7 +187,10 @@ namespace OS { ? $(this).prev()[0] : undefined; } - + if(this.dir) + { + return; + } if (tagname === "AFX-HBOX") { this.dir = "hz"; } else if (tagname === "AFX-VBOX") { @@ -197,6 +200,20 @@ namespace OS { } } + /** + * Setter Disable or enable the resize event + * + * @memberof ResizerTag + */ + set disable(v: boolean) + { + this.attsw(v, "disable"); + } + get disable(): boolean + { + return this.hasattr("disable"); + } + /** * Enable draggable on the element * @@ -210,6 +227,10 @@ namespace OS { } $(this).on("pointerdown", (e) => { e.preventDefault(); + if(this.disable) + { + return; + } $(window).on("pointermove", (evt) => { if (!this._resizable_el) { return; diff --git a/src/core/tags/TabBarTag.ts b/src/core/tags/TabBarTag.ts index ca6d38a..b32e448 100644 --- a/src/core/tags/TabBarTag.ts +++ b/src/core/tags/TabBarTag.ts @@ -257,6 +257,42 @@ namespace OS { }); } + /** + * Scroll the tabbar to end + * + * @memberof TabBarTag + */ + scroll_to_end() + { + const list_container = $(".list-container", this.refs.list)[0]; + if(this.dir == "vertical") + { + list_container.scrollTo({ top: list_container.scrollHeight, behavior: 'smooth' }); + } + else + { + list_container.scrollTo({ left: list_container.scrollWidth, behavior: 'smooth' }); + } + } + + /** + * Scroll the tabbar to begin + * + * @memberof TabBarTag + */ + scroll_to_start() + { + const list_container = $(".list-container", this.refs.list)[0]; + if(this.dir == "vertical") + { + list_container.scrollTo({ top: 0, behavior: 'smooth' }); + } + else + { + list_container.scrollTo({ left: 0, behavior: 'smooth' }); + } + } + /** * TabBar layout definition * diff --git a/src/core/tags/TileLayoutTags.ts b/src/core/tags/TileLayoutTags.ts index 9671bb7..bd5e675 100644 --- a/src/core/tags/TileLayoutTags.ts +++ b/src/core/tags/TileLayoutTags.ts @@ -1,6 +1,9 @@ namespace OS { export namespace GUI { export namespace tag { + + type TileItemDirection = "row" | "column" | "row-reverse" | "column-reverse"; + /** * A tile layout organize it child elements * in a fixed horizontal or vertical direction. @@ -77,15 +80,15 @@ namespace OS { * * @memberof TileLayoutTag */ - set dir(v: "row" | "column") { + set dir(v: TileItemDirection) { if (!v) { return; } $(this).attr("dir", v); - $(this.refs.yield).css("flex-direction", v); + this.reversed = this.reversed; this.calibrate(); } - get dir(): "row" | "column" { + get dir(): TileItemDirection { return $(this).attr("dir") as any; } /** @@ -108,6 +111,35 @@ namespace OS { { return this._padding; } + /** + * setter: Reverse order of the content in the tile + * + * getter: return if the tile's content is in reversed order + * + * @meberof TileLayoutTags + */ + set reversed(v: boolean) + { + this.attsw(v, "reversed"); + if(!this.dir) + { + return; + } + let newdir = "row"; + if(this.dir.startsWith("column")) + { + newdir = "column" + } + if(v) + { + newdir += "-reverse"; + } + $(this.refs.yield).css("flex-direction", newdir); + } + get reversed(): boolean + { + return this.hasattr("reversed"); + } /** * Mount the element diff --git a/src/core/tags/WindowTag.ts b/src/core/tags/WindowTag.ts index 842fb1f..b0bcd6b 100644 --- a/src/core/tags/WindowTag.ts +++ b/src/core/tags/WindowTag.ts @@ -468,6 +468,7 @@ namespace OS { this.observable.trigger("resize", { id: this.aid, data: o, + root: true }); } diff --git a/src/core/tags/tag.ts b/src/core/tags/tag.ts index 1b4c392..3157c10 100644 --- a/src/core/tags/tag.ts +++ b/src/core/tags/tag.ts @@ -242,6 +242,13 @@ namespace OS { * @memberof TagEventType */ originalEvent?: any; + /** + * is root tag? + * + * @type {boolean} + * @memberof TagEventType + */ + root?: boolean; } /** @@ -270,8 +277,163 @@ namespace OS { } /** * Tag event callback type + * + * @export */ export type TagEventCallback = (e: TagEventType) => void; + /** + * Tag responsive envent callback type + * + * @export + */ + export type TagResponsiveCallback = (fullfilled: boolean) => void; + + /** + * A callback record with history of last fulfilled + * + * @interface TagResponsiveCallbackRecord + */ + interface TagResponsiveCallbackRecord { + /** + * Callback function + * + * @type {TagResponsiveCallback} + * @memberof TagResponsiveCallbackRecord + */ + callback: TagResponsiveCallback, + /** + * has the event been previously fulfilled? + * + * @type {boolean} + * @memberof TagResponsiveCallbackRecord + */ + fulfilled: boolean, + } + + /** + * Tag responsive validator type + * + * @export + */ + export type TagResponsiveValidator = (w: number, h: number) => boolean; + + /** + * Definitions of some default tag responsive validators + * + * @export + */ + + export const RESPONSIVE = { + /** + * Extra small devices (phones, 600px and down) + */ + TINY: function(w: number,_h: number){ + return w <= 600; + }, + /* + Small devices (portrait tablets and large phones, 600px and up) + */ + SMALL: function(w: number, _h: number){ + return w <= 768; + }, + /* + Medium devices (landscape tablets, 768px and up) + */ + MEDIUM: function(w: number, _h: number){ + return w > 768; + }, + /** + * Large devices (laptops/desktops, 992px and up) + */ + LARGE: function(w: number, _h: number){ + return w > 992; + }, + /** + * Extra large devices (large laptops and desktops, 1200px and up) + */ + HUGE: function(w: number, _h: number){ + return w > 1200; + }, + /** + * Portrait mode + */ + PORTRAIT: function(w: number, h: number){ + return h > w; + }, + /** + * Landscape mode + */ + LANDSCAPE: function(w: number, h: number){ + return h < w; + }, + } + + /** + * A custom map to handle responsive events, a responsive event + * consists of a validator and a callback + * + * When avalidator return true, its corresponding callback will + * be called + */ + class ResponsiveHandle extends Map { + /** + * Register a responsive envent to this map + * + * @param {TagResponsiveValidator} a validator + * @param {TagResponsiveCallback} event callback + * @memberof ResponsiveHandle + */ + on(validator: TagResponsiveValidator, callback: TagResponsiveCallback) + { + let record: TagResponsiveCallbackRecord = { + callback: callback, + fulfilled: false, + } + this.set(validator, record); + } + + /** + * unregister a responsive event + * + * @param {TagResponsiveValidator} a validator + * @memberof ResponsiveHandle + */ + off(validator: TagResponsiveValidator) + { + this.delete(validator); + } + + /** + * Verify validators and execute callbacks + * + * @param {number} root tag width + * @param {number} root tag height + * @memberof ResponsiveHandle + */ + morph(rootw: number, rooth: number) + { + for (const [validator, record] of this.entries()) { + const val = validator(rootw, rooth); + if(record.fulfilled && val) + { + // the record has been previously fulfilled + // and the validator returns true + // nothing changed + continue; + } + if(!record.fulfilled && !val) + { + // the record has been previously unfulfilled + // and the validator returns false + // nothing changed + continue; + } + record.fulfilled =val; + record.callback(val); + } + } + } + /** * Base abstract class for tag implementation, any AFX tag should be * subclass of this class @@ -313,7 +475,17 @@ namespace OS { protected _mounted: boolean; /** - *Creates an instance of AFXTag. + * a {@link ResponsiveHandle} to handle all responsive event + * related to this tag + * + * @private + * @memberof AFXTag + */ + private _responsive_handle: ResponsiveHandle; + + private _responsive_check: (evt: TagEventType<{w: number, h: number}>) => void; + /** + * Creates an instance of AFXTag. * @memberof AFXTag */ constructor() { @@ -324,6 +496,8 @@ namespace OS { } this._mounted = false; this.refs = {}; + this._responsive_handle = new ResponsiveHandle(); + this._responsive_check = undefined; } /** @@ -394,6 +568,60 @@ namespace OS { return $(this).attr("data-id"); } + /** + * Setter: Enable/disable responsive tag + * + * Getter: get responsive status + */ + set responsive(v:boolean) { + if(!v) + { + this.attsw(false,"responsive"); + this.observable.off("resize",this._responsive_check); + this._responsive_check = undefined; + return; + } + if(this._responsive_check) + { + return; + } + this._responsive_check = (evt: TagEventType<{w: number, h: number}>) => { + if(!evt || !evt.root || !evt.data.w || !evt.data.h ) + { + return; + } + this._responsive_handle.morph(evt.data.w, evt.data.h); + } + this.observable.on("resize", this._responsive_check); + this.attsw(true, "responsive"); + } + get responsive(): boolean { + return this.hasattr("responsive"); + } + + /** + * Register a responsive event to this tag + * + * @param {TagResponsiveValidator} responsive validator + * @param {TagResponsiveCallback} responsive callback + * @memberof AFXTag + */ + morphon(validator: TagResponsiveValidator, callback:TagResponsiveCallback) + { + this._responsive_handle.on(validator, callback); + } + + /** + * Unregister a responsive event from this tag + * + * @param {TagResponsiveValidator} responsive validator + * @memberof AFXTag + */ + morphoff(validator: TagResponsiveValidator) + { + this._responsive_handle.off(validator); + } + /** * Attach a data to this tag *