UNPKG

uicore-ts

Version:

UICore is a library to build native-like user interfaces using pure Typescript. No HTML is needed at all. Components are described as TS classes and all user interactions are handled explicitly. This library is strongly inspired by the UIKit framework tha

1,714 lines (1,218 loc) 112 kB
import { IS_FIREFOX, IS_SAFARI } from "./ClientCheckers" import toPx from "./SizeConverter" import { UIColor } from "./UIColor" import { UICore } from "./UICore" import "./UICoreExtensions" import type { UIDialogView } from "./UIDialogView" import { UILocalizedTextObject } from "./UIInterfaces" import { FIRST, FIRST_OR_NIL, IF, IS, IS_DEFINED, IS_NIL, IS_NOT, nil, NO, RETURNER, UIObject, YES } from "./UIObject" import { UIPoint } from "./UIPoint" import { UIRectangle } from "./UIRectangle" import { UIViewController } from "./UIViewController" declare module AutoLayout { class Constraint { [key: string]: any } class View { [key: string]: any } class VisualFormat { static parse(arg0: any, arg1: any): any; [key: string]: any } const enum Attribute { LEFT = 0, RIGHT = 1, BOTTOM = 2, TOP = 3, CENTERX = 4, CENTERY = 5, WIDTH = 6, HEIGHT = 7, ZINDEX = 8, VARIABLE = 9, NOTANATTRIBUTE = 10 } const enum Relation { EQU = 0, LEQ = 1, GEQ = 2 } } // @ts-ignore if (!window.AutoLayout) { // @ts-ignore window.AutoLayout = nil } declare global { interface HTMLElement { UIViewObject?: UIView; } } export interface LooseObject { [key: string]: any } export interface ControlEventTargetsObject { [key: string]: Function[]; } export interface UIViewBroadcastEvent { name: string; parameters?: { [key: string]: string | string[]; } } type Mutable<T> = { -readonly [P in keyof T]: T[P] }; export type UIViewAddControlEventTargetObject<T extends { controlEvent: Record<string, any> }> = Mutable<{ [K in keyof T["controlEvent"]]: (( sender: UIView, event: Event ) => void) & Partial<UIViewAddControlEventTargetObject<T>> }> interface Constraint { constant: number; multiplier: number; view1: any; attr2: any; priority: number; attr1: any; view2: any; relation: any } export function UIComponentView(target: Function, context: ClassDecoratorContext) { console.log("Recording annotation UIComponentView on " + target.name) UIObject.recordAnnotation(UIComponentView, target) } export class UIView extends UIObject { _nativeSelectionEnabled: boolean = YES _shouldLayout?: boolean _UITableViewRowIndex?: number _UITableViewReusabilityIdentifier: any _UIViewIntrinsicTemporaryWidth?: string _UIViewIntrinsicTemporaryHeight?: string _enabled: boolean = YES _frame?: UIRectangle & { zIndex?: number } _backgroundColor: UIColor = UIColor.transparentColor _viewHTMLElement!: HTMLElement & LooseObject // Dynamic innerHTML _innerHTMLKey?: string _defaultInnerHTML?: string _parameters?: { [x: string]: (string | UILocalizedTextObject) } _localizedTextObject?: UILocalizedTextObject _controlEventTargets: ControlEventTargetsObject = {} //{ "PointerDown": Function[]; "PointerMove": Function[]; "PointerLeave": Function[]; "PointerEnter": Function[]; "PointerUpInside": Function[]; "PointerUp": Function[]; "PointerHover": Function[]; }; _frameTransform: string viewController?: UIViewController _updateLayoutFunction?: Function // @ts-ignore _constraints: any[] //AutoLayout.Constraint[]; superview?: UIView subviews: UIView[] = [] _styleClasses: any[] _isHidden: boolean = NO pausesPointerEvents: boolean = NO stopsPointerEventPropagation: boolean = YES pointerDraggingPoint: UIPoint = new UIPoint(0, 0) _previousClientPoint: UIPoint = new UIPoint(0, 0) _isPointerInside?: boolean _isPointerValid?: boolean _isPointerValidForMovement?: boolean _isPointerDown = NO _initialPointerPosition?: UIPoint _hasPointerDragged?: boolean _pointerDragThreshold = 2 ignoresTouches: boolean = NO ignoresMouse: boolean = NO core: UICore = UICore.main static _UIViewIndex: number = -1 _UIViewIndex: number static _viewsToLayout: UIView[] = [] forceIntrinsicSizeZero: boolean = NO _touchEventTime?: number static _pageScale = 1 _scale: number = 1 isInternalScaling: boolean = YES static resizeObserver = new ResizeObserver((entries, observer) => { for (let i = 0; i < entries.length; i++) { const entry = entries[i] // @ts-ignore const view: UIView = entry.target.UIView view?.didResize?.(entry) } }) private _isMovable = NO makeNotMovable?: () => void private _isResizable = NO makeNotResizable?: () => void resizingHandles: UIView[] = [] private _isMoving: boolean = NO _isCBEditorTemporaryResizable = NO _isCBEditorTemporaryMovable = NO static _onWindowTouchMove: (event: TouchEvent) => void = nil static _onWindowMouseMove: (event: MouseEvent) => void = nil static _onWindowMouseup: (event: MouseEvent) => void = nil private _resizeObserverEntry?: ResizeObserverEntry private _intrinsicSizesCache: Record<string, UIRectangle> = {} constructor( elementID: string = ("UIView" + UIView.nextIndex), viewHTMLElement: HTMLElement & LooseObject | null = null, elementType: string | null = null, initViewData?: any ) { super() // Instance variables UIView._UIViewIndex = UIView.nextIndex this._UIViewIndex = UIView._UIViewIndex this._styleClasses = [] this._initViewHTMLElement(elementID, viewHTMLElement, elementType) this._constraints = [] this._frameTransform = "" this._initViewCSSSelectorsIfNeeded() this._loadUIEvents() this.setNeedsLayout() } static get nextIndex() { return UIView._UIViewIndex + 1 } static get pageHeight() { const body = document.body const html = document.documentElement return Math.max( body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight ) } static get pageWidth() { const body = document.body const html = document.documentElement return Math.max(body.scrollWidth, body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth) } centerInContainer() { this.style.left = "50%" this.style.top = "50%" this.style.transform = "translateX(-50%) translateY(-50%)" } centerXInContainer() { this.style.left = "50%" this.style.transform = "translateX(-50%)" } centerYInContainer() { this.style.top = "50%" this.style.transform = "translateY(-50%)" } _initViewHTMLElement( elementID: string, viewHTMLElement: (HTMLElement & LooseObject) | null, elementType?: string | null ) { if (!IS(elementType)) { elementType = "div" } if (!IS(viewHTMLElement)) { this._viewHTMLElement = this.createElement(elementID, elementType) this.style.position = "absolute" this.style.margin = "0" } else { this._viewHTMLElement = viewHTMLElement } if (IS(elementID)) { this.viewHTMLElement.id = elementID } this.viewHTMLElement.obeyAutolayout = YES this.viewHTMLElement.UIViewObject = this this.addStyleClass(this.styleClassName) } set nativeSelectionEnabled(selectable: boolean) { this._nativeSelectionEnabled = selectable if (!selectable) { this.style.cssText = this.style.cssText + " -webkit-touch-callout: none; -webkit-user-select: none; " + "-khtml-user-select: none; -moz-user-select: none; " + "-ms-user-select: none; user-select: none;" } else { this.style.cssText = this.style.cssText + " -webkit-touch-callout: text; -webkit-user-select: text; " + "-khtml-user-select: text; -moz-user-select: text; " + "-ms-user-select: text; user-select: text;" } } get nativeSelectionEnabled() { return this._nativeSelectionEnabled } get styleClassName() { return "UICore_UIView_" + this.class.name } protected _initViewCSSSelectorsIfNeeded() { if (!this.class._areViewCSSSelectorsInitialized) { this.initViewStyleSelectors() this.class._areViewCSSSelectorsInitialized = YES } } initViewStyleSelectors() { // Override this in a subclass } initStyleSelector(selector: string, style: string) { const styleRules = UIView.getStyleRules(selector) if (!styleRules) { UIView.createStyleSelector(selector, style) } } createElement(elementID: string, elementType: string) { let result = document.getElementById(elementID) if (!result) { result = document.createElement(elementType) } return result } public get viewHTMLElement() { return this._viewHTMLElement } public get elementID() { return this.viewHTMLElement.id } setInnerHTML(key: string, defaultString: string, parameters?: { [x: string]: string | UILocalizedTextObject }) { this._innerHTMLKey = key this._defaultInnerHTML = defaultString this._parameters = parameters const languageName = UICore.languageService.currentLanguageKey const result = UICore.languageService.stringForKey(key, languageName, defaultString, parameters) this.innerHTML = result ?? "" } protected _setInnerHTMLFromKeyIfPossible() { if (this._innerHTMLKey && this._defaultInnerHTML) { this.setInnerHTML(this._innerHTMLKey, this._defaultInnerHTML, this._parameters) } } protected _setInnerHTMLFromLocalizedTextObjectIfPossible() { if (IS(this._localizedTextObject)) { this.innerHTML = UICore.languageService.stringForCurrentLanguage(this._localizedTextObject) } } get localizedTextObject() { return this._localizedTextObject } set localizedTextObject(localizedTextObject: UILocalizedTextObject | undefined) { this._localizedTextObject = localizedTextObject this._setInnerHTMLFromLocalizedTextObjectIfPossible() } get innerHTML() { return this.viewHTMLElement.innerHTML } set innerHTML(innerHTML) { if (this.innerHTML != innerHTML) { this.viewHTMLElement.innerHTML = FIRST(innerHTML, "") } } set hoverText(hoverText: string) { this.viewHTMLElement.setAttribute("title", hoverText) } get hoverText() { return this.viewHTMLElement.getAttribute("title") ?? "" } get scrollSize() { return new UIRectangle(0, 0, this.viewHTMLElement.scrollHeight, this.viewHTMLElement.scrollWidth) } get dialogView(): UIDialogView | undefined { if (!IS(this.superview)) { return } if (!((this as any)["_isAUIDialogView"])) { return this.superview.dialogView } return this as any as UIDialogView } get rootView(): UIView { if (IS(this.superview)) { return this.superview.rootView } return this } public set enabled(enabled: boolean) { this._enabled = enabled this.updateContentForCurrentEnabledState() } public get enabled(): boolean { return this._enabled } updateContentForCurrentEnabledState() { this.hidden = !this.enabled this.userInteractionEnabled = this.enabled } public get tabIndex(): number { return Number(this.viewHTMLElement.getAttribute("tabindex")) } public set tabIndex(index: number) { this.viewHTMLElement.setAttribute("tabindex", "" + index) } get propertyDescriptors(): { object: UIObject; name: string }[] { let result: any[] = [] function isPlainObject(value: any): value is object { return value instanceof Object && Object.getPrototypeOf(value) === Object.prototype } function isAnArray(value: any): value is any[] { return value instanceof Array && Object.getPrototypeOf(value) === Array.prototype } this.withAllSuperviews.forEach(view => { const descriptorFromObject = function ( this: UIView, object?: object, pathRootObject: object = object!, existingPathComponents: string[] = [], lookInArrays = YES, depthLeft = 5 ): { subObjects: { object: object; key: string }[]; descriptor: { name: string; object: object } | undefined } { let resultDescriptor: { name: string; object: object } | undefined const subObjects: { object: object, key: string }[] = [] const subArrays: { array: any[], key: string }[] = [] FIRST_OR_NIL(object).forEach((value, key, stopLooping) => { if (this == value) { existingPathComponents.push(key) resultDescriptor = { object: pathRootObject!, name: existingPathComponents.join(".") } stopLooping() return } if ( isPlainObject(value) && !_UIViewPropertyKeys.contains(key) && !_UIViewControllerPropertyKeys.contains(key) ) { subObjects.push({ object: value, key: key }) } if ( lookInArrays && isAnArray(value) && !_UIViewPropertyKeys.contains(key) && !_UIViewControllerPropertyKeys.contains(key) ) { subArrays.push({ array: value, key: key }) } }) if (!resultDescriptor && lookInArrays) { subArrays.copy().forEach(value => { if (value.key.startsWith("_")) { subArrays.removeElement(value) subArrays.push(value) } }) subArrays.find(arrayObject => arrayObject.array.find((value, index) => { if (this == value) { existingPathComponents.push(arrayObject.key + "." + index) resultDescriptor = { object: pathRootObject!, name: existingPathComponents.join(".") } return YES } return NO }) ) } // if (!resultDescriptor && depthLeft) { // // // @ts-ignore // resultDescriptor = subObjects.find(object => descriptorFromObject( // object, // pathRootObject, // existingPathComponents, // NO, // depthLeft - 1 // )) // // } if (resultDescriptor?.object?.constructor?.name == "Object") { var asd = 1 } const result = { descriptor: resultDescriptor, subObjects: subObjects } return result }.bind(this) const viewControllerResult = descriptorFromObject(view.viewController) if (viewControllerResult?.descriptor) { result.push(viewControllerResult.descriptor) } const viewResult = descriptorFromObject(view) if (viewResult?.descriptor) { result.push(viewResult.descriptor) } else if (this.superview && this.superview == view) { result.push({ object: view, key: "subviews." + view.subviews.indexOf(this) }) } // view.forEach((value, key, stopLooping) => { // // if (this == value) { // // result.push({ object: view, name: key }) // // stopLooping() // // } // // }) }) return result } get styleClasses() { return this._styleClasses } set styleClasses(styleClasses) { this._styleClasses = styleClasses } hasStyleClass(styleClass: string) { // This is for performance reasons if (!IS(styleClass)) { return NO } const index = this.styleClasses.indexOf(styleClass) if (index > -1) { return YES } return NO } addStyleClass(styleClass: string) { if (!IS(styleClass)) { return } if (!this.hasStyleClass(styleClass)) { this._styleClasses.push(styleClass) } } removeStyleClass(styleClass: string) { // This is for performance reasons if (!IS(styleClass)) { return } const index = this.styleClasses.indexOf(styleClass) if (index > -1) { this.styleClasses.splice(index, 1) } } static findViewWithElementID(elementID: string): UIView | undefined { const viewHTMLElement = document.getElementById(elementID) if (IS_NOT(viewHTMLElement)) { return } // @ts-ignore return viewHTMLElement.UIView } static createStyleSelector(selector: string, style: string) { return // // @ts-ignore // if (!document.styleSheets) { // return // } // if (document.getElementsByTagName("head").length == 0) { // return // } // // let styleSheet // let mediaType // // if (document.styleSheets.length > 0) { // for (var i = 0, l: any = document.styleSheets.length; i < l; i++) { // if (document.styleSheets[i].disabled) { // continue // } // const media = document.styleSheets[i].media // mediaType = typeof media // // if (mediaType === "string") { // if (media as any === "" || ((media as any).indexOf("screen") !== -1)) { // styleSheet = document.styleSheets[i] // } // } // else if (mediaType == "object") { // if (media.mediaText === "" || (media.mediaText.indexOf("screen") !== -1)) { // styleSheet = document.styleSheets[i] // } // } // // if (typeof styleSheet !== "undefined") { // break // } // } // } // // if (typeof styleSheet === "undefined") { // const styleSheetElement = document.createElement("style") // styleSheetElement.type = "text/css" // document.getElementsByTagName("head")[0].appendChild(styleSheetElement) // // for (i = 0; i < document.styleSheets.length; i++) { // if (document.styleSheets[i].disabled) { // continue // } // styleSheet = document.styleSheets[i] // } // // mediaType = typeof styleSheet.media // } // // if (mediaType === "string") { // for (var i = 0, l = styleSheet.rules.length; i < l; i++) { // if (styleSheet.rules[i].selectorText && styleSheet.rules[i].selectorText.toLowerCase() == // selector.toLowerCase()) { // styleSheet.rules[i].style.cssText = style // return // } // } // styleSheet.addRule(selector, style) // } // else if (mediaType === "object") { // // var styleSheetLength = 0 // // try { // // styleSheetLength = (styleSheet.cssRules) ? styleSheet.cssRules.length : 0 // // } catch (error) { // // } // // // for (var i = 0; i < styleSheetLength; i++) { // if (styleSheet.cssRules[i].selectorText && styleSheet.cssRules[i].selectorText.toLowerCase() == // selector.toLowerCase()) { // styleSheet.cssRules[i].style.cssText = style // return // } // } // styleSheet.insertRule(selector + "{" + style + "}", styleSheetLength) // } } static getStyleRules(selector: string) { // https://stackoverflow.com/questions/324486/how-do-you-read-css-rule-values-with-javascript //Inside closure so that the inner functions don't need regeneration on every call. const getCssClasses = (function () { function normalize(str: string) { if (!str) { return "" } str = String(str).replace(/\s*([>~+])\s*/g, " $1 ") //Normalize symbol spacing. return str.replace(/(\s+)/g, " ").trim() //Normalize whitespace } function split(str: string, on: string) { //Split, Trim, and remove empty elements return str.split(on).map(x => x.trim()).filter(x => x) } function containsAny(selText: string | any[], ors: any[]) { return selText ? ors.some(x => selText.indexOf(x) >= 0) : false } return function (selector: string) { const logicalORs = split(normalize(selector), ",") const sheets = Array.from(window.document.styleSheets) const ruleArrays = sheets.map((x) => Array.from(x.rules || x.cssRules || [])) const allRules = ruleArrays.reduce((all, x) => all.concat(x), []) // @ts-ignore return allRules.filter((x) => containsAny(normalize(x.selectorText), logicalORs)) } })() return getCssClasses(selector) // selector = selector.toLowerCase() // let styleRules // for (let i = 0; i < document.styleSheets.length; i++) { // const styleSheet = document.styleSheets[i] as any // // try { // // styleRules = styleSheet.cssRules ? styleSheet.cssRules : styleSheet.rules // // } catch (error) { // // console.log(error) // // } // // } // // return styleRules } get style() { return this.viewHTMLElement.style } get computedStyle() { return getComputedStyle(this.viewHTMLElement) } public get hidden(): boolean { return this._isHidden } public set hidden(v: boolean) { this._isHidden = v if (this._isHidden) { this.style.visibility = "hidden" } else { this.style.visibility = "visible" } } static set pageScale(scale: number) { UIView._pageScale = scale const zoom = scale const width = 100 / zoom const viewHTMLElement = UICore.main.rootViewController.view.viewHTMLElement viewHTMLElement.style.transformOrigin = "left top" viewHTMLElement.style.transform = "scale(" + zoom + ")" viewHTMLElement.style.width = width + "%" } static get pageScale() { return UIView._pageScale } set scale(scale: number) { this._scale = scale const zoom = scale // const width = 100 / zoom // const height = 100 / zoom const viewHTMLElement = this.viewHTMLElement viewHTMLElement.style.transformOrigin = "left top" viewHTMLElement.style.transform = viewHTMLElement.style.transform.replace( (viewHTMLElement.style.transform.match( new RegExp("scale\\(.*\\)", "g") )?.firstElement ?? ""), "" ) + "scale(" + zoom + ")" // viewHTMLElement.style.width = width + "%" // viewHTMLElement.style.height = height + "%" if (this.isInternalScaling) { this.setFrame(this.frame, this.frame.zIndex, YES) } } get scale() { return this._scale } // Use this method to calculate the frame for the view itself // This can be used when adding subviews to existing views like buttons calculateAndSetViewFrame() { } public get frame() { let result: UIRectangle & { zIndex?: number } = this._frame?.copy() as any if (!result) { result = new UIRectangle( this._resizeObserverEntry?.contentRect.left ?? this.viewHTMLElement.offsetLeft, this._resizeObserverEntry?.contentRect.top ?? this.viewHTMLElement.offsetTop, this._resizeObserverEntry?.contentRect.height ?? this.viewHTMLElement.offsetHeight, this._resizeObserverEntry?.contentRect.width ?? this.viewHTMLElement.offsetWidth ) as any result.zIndex = 0 } return result } public set frame(rectangle: UIRectangle & { zIndex?: number }) { if (IS(rectangle)) { this.setFrame(rectangle, rectangle.zIndex) } } setFrame(rectangle: UIRectangle & { zIndex?: number }, zIndex = 0, performUncheckedLayout = NO) { const frame: (UIRectangle & { zIndex?: number }) = this._frame || new UIRectangle(nil, nil, nil, nil) as any if (zIndex != undefined) { rectangle.zIndex = zIndex } this._frame = rectangle if (frame && frame != nil && frame.isEqualTo(rectangle) && frame.zIndex == rectangle.zIndex && !performUncheckedLayout) { return } if (this.isInternalScaling) { rectangle.scale(1 / this.scale) } UIView._setAbsoluteSizeAndPosition( this.viewHTMLElement, rectangle.topLeft.x, rectangle.topLeft.y, rectangle.width, rectangle.height, rectangle.zIndex, this.frameTransform ) if (frame.height != rectangle.height || frame.width != rectangle.width || performUncheckedLayout) { this.setNeedsLayout() this.boundsDidChange(this.bounds) } } get bounds() { let result: UIRectangle if (IS_NOT(this._frame)) { result = new UIRectangle( 0, 0, this._resizeObserverEntry?.contentRect.height ?? this.viewHTMLElement.offsetHeight, this._resizeObserverEntry?.contentRect.width ?? this.viewHTMLElement.offsetWidth ) } else { result = this.frame.copy() result.x = 0 result.y = 0 } return result } set bounds(rectangle: UIRectangle) { const frame = this.frame const newFrame = new UIRectangle(frame.topLeft.x, frame.topLeft.y, rectangle.height, rectangle.width) // @ts-ignore newFrame.zIndex = frame.zIndex this.frame = newFrame } boundsDidChange(bounds: UIRectangle) { } didResize(entry: ResizeObserverEntry) { this._resizeObserverEntry = entry this.setNeedsLayout() this.boundsDidChange(new UIRectangle(0, 0, entry.contentRect.height, entry.contentRect.width)) } get frameTransform(): string { return this._frameTransform } set frameTransform(value: string) { this._frameTransform = value this.setFrame(this.frame, this.frame.zIndex, YES) } setPosition( left: number | string = nil, right: number | string = nil, bottom: number | string = nil, top: number | string = nil, height: number | string = nil, width: number | string = nil ) { const previousBounds = this.bounds this.setStyleProperty("left", left) this.setStyleProperty("right", right) this.setStyleProperty("bottom", bottom) this.setStyleProperty("top", top) this.setStyleProperty("height", height) this.setStyleProperty("width", width) const bounds = this.bounds if (bounds.height != previousBounds.height || bounds.width != previousBounds.width) { this.setNeedsLayout() this.boundsDidChange(bounds) } } setSizes(height?: number | string, width?: number | string) { const previousBounds = this.bounds this.setStyleProperty("height", height) this.setStyleProperty("width", width) const bounds = this.bounds if (bounds.height != previousBounds.height || bounds.width != previousBounds.width) { this.setNeedsLayout() this.boundsDidChange(bounds) } } setMinSizes(height?: number | string, width?: number | string) { const previousBounds = this.bounds this.setStyleProperty("minHeight", height) this.setStyleProperty("minWidth", width) const bounds = this.bounds if (bounds.height != previousBounds.height || bounds.width != previousBounds.width) { this.setNeedsLayout() this.boundsDidChange(bounds) } } setMaxSizes(height?: number | string, width?: number | string) { const previousBounds = this.bounds this.setStyleProperty("maxHeight", height) this.setStyleProperty("maxWidth", width) const bounds = this.bounds if (bounds.height != previousBounds.height || bounds.width != previousBounds.width) { this.setNeedsLayout() this.boundsDidChange(bounds) } } setMargin(margin?: number | string) { const previousBounds = this.bounds this.setStyleProperty("margin", margin) const bounds = this.bounds if (bounds.height != previousBounds.height || bounds.width != previousBounds.width) { this.setNeedsLayout() this.boundsDidChange(bounds) } } setMargins(left?: number | string, right?: number | string, bottom?: number | string, top?: number | string) { const previousBounds = this.bounds this.setStyleProperty("marginLeft", left) this.setStyleProperty("marginRight", right) this.setStyleProperty("marginBottom", bottom) this.setStyleProperty("marginTop", top) const bounds = this.bounds if (bounds.height != previousBounds.height || bounds.width != previousBounds.width) { this.setNeedsLayout() this.boundsDidChange(bounds) } } setPadding(padding?: number | string) { const previousBounds = this.bounds this.setStyleProperty("padding", padding) const bounds = this.bounds if (bounds.height != previousBounds.height || bounds.width != previousBounds.width) { this.setNeedsLayout() this.boundsDidChange(bounds) } } setPaddings(left?: number | string, right?: number | string, bottom?: number | string, top?: number | string) { const previousBounds = this.bounds this.setStyleProperty("paddingLeft", left) this.setStyleProperty("paddingRight", right) this.setStyleProperty("paddingBottom", bottom) this.setStyleProperty("paddingTop", top) const bounds = this.bounds if (bounds.height != previousBounds.height || bounds.width != previousBounds.width) { this.setNeedsLayout() this.boundsDidChange(bounds) } } setBorder( radius: number | string = nil, width: number | string = 1, color: UIColor = UIColor.blackColor, style: string = "solid" ) { this.setStyleProperty("borderStyle", style) this.setStyleProperty("borderRadius", radius) this.setStyleProperty("borderColor", color.stringValue) this.setStyleProperty("borderWidth", width) } setStyleProperty(propertyName: string, value?: number | string) { try { if (IS_NIL(value)) { return } if (IS_DEFINED(value) && (value as Number).isANumber) { value = "" + (value as number).integerValue + "px" } // @ts-ignore this.style[propertyName] = value } catch (exception) { console.log(exception) } } get userInteractionEnabled() { return (this.style.pointerEvents != "none") } set userInteractionEnabled(userInteractionEnabled) { if (userInteractionEnabled) { this.style.pointerEvents = "" } else { this.style.pointerEvents = "none" } } get backgroundColor() { return this._backgroundColor } set backgroundColor(backgroundColor: UIColor) { this._backgroundColor = backgroundColor this.style.backgroundColor = backgroundColor.stringValue } get alpha() { return 1 * (this.style.opacity as any) } set alpha(alpha) { this.style.opacity = "" + alpha } static animateViewOrViewsWithDurationDelayAndFunction( viewOrViews: UIView | HTMLElement | UIView[] | HTMLElement[], duration: number, delay: number, timingStyle = "cubic-bezier(0.25,0.1,0.25,1)", transformFunction: Function, transitioncompletionFunction: Function ) { function callTransitioncompletionFunction() { (transitioncompletionFunction || nil)(); (viewOrViews as UIView[] | HTMLElement[]).forEach(view => { if (view instanceof UIView) { view.animationDidFinish() } }) } if (IS_FIREFOX) { // Firefox does not fire the transition completion event properly new UIObject().performFunctionWithDelay(delay + duration, callTransitioncompletionFunction) } if (!(viewOrViews instanceof Array)) { viewOrViews = [viewOrViews] as any } const transitionStyles: any[] = [] const transitionDurations: any[] = [] const transitionDelays: any[] = [] const transitionTimings: any[] = [] function isUIView(view: any): view is UIView { return IS(view.viewHTMLElement) } for (var i = 0; i < (viewOrViews as any).length; i++) { let view = (viewOrViews as UIView[] | HTMLElement[])[i] if (isUIView(view)) { view = view.viewHTMLElement } // @ts-ignore view.addEventListener("transitionend", transitionDidFinish, true) transitionStyles.push(view.style.transition) transitionDurations.push(view.style.transitionDuration) transitionDelays.push(view.style.transitionDelay) transitionTimings.push(view.style.transitionTimingFunction) view.style.transition = "all" view.style.transitionDuration = "" + duration + "s" view.style.transitionDelay = "" + delay + "s" view.style.transitionTimingFunction = timingStyle } transformFunction() const transitionObject = { "finishImmediately": finishTransitionImmediately, "didFinish": transitionDidFinishManually, "views": viewOrViews, "registrationTime": Date.now() } function finishTransitionImmediately() { for (var i = 0; i < (viewOrViews as any).length; i++) { let view = (viewOrViews as UIView[] | HTMLElement[])[i] if (isUIView(view)) { view = view.viewHTMLElement } view.style.transition = "all" view.style.transitionDuration = "" + duration + "s" view.style.transitionDelay = "" + delay + "s" view.style.transition = transitionStyles[i] view.style.transitionDuration = transitionDurations[i] view.style.transitionDelay = transitionDelays[i] view.style.transitionTimingFunction = transitionTimings[i] } } function transitionDidFinish(this: HTMLElement, event: { srcElement: HTMLElement | UIView }) { let view = event.srcElement if (!view) { return } if (isUIView(view)) { view = view.viewHTMLElement } view.style.transition = transitionStyles[i] view.style.transitionDuration = transitionDurations[i] view.style.transitionDelay = transitionDelays[i] view.style.transitionTimingFunction = transitionTimings[i] callTransitioncompletionFunction() // @ts-ignore view.removeEventListener("transitionend", transitionDidFinish, true) } function transitionDidFinishManually() { for (let i = 0; i < (viewOrViews as any).length; i++) { let view = (viewOrViews as UIView[] | HTMLElement[])[i] if (isUIView(view)) { view = view.viewHTMLElement } view.style.transition = transitionStyles[i] view.style.transitionDuration = transitionDurations[i] view.style.transitionDelay = transitionDelays[i] view.style.transitionTimingFunction = transitionTimings[i] // @ts-ignore view.removeEventListener("transitionend", transitionDidFinish, true) } } return transitionObject } animationDidFinish() { } static _transformAttribute = (("transform" in document.documentElement.style) ? "transform" : undefined) || (("-webkit-transform" in document.documentElement.style) ? "-webkit-transform" : "undefined") || (("-moz-transform" in document.documentElement.style) ? "-moz-transform" : "undefined") || (("-ms-transform" in document.documentElement.style) ? "-ms-transform" : "undefined") || (("-o-transform" in document.documentElement.style) ? "-o-transform" : "undefined") static _setAbsoluteSizeAndPosition( element: HTMLElement & LooseObject, left: number, top: number, width: string | number, height: string | number, zIndex = 0, frameTransform = "" ) { if (!IS(element) || !element.obeyAutolayout && !element.getAttribute("obeyAutolayout")) { return } if (IS(height)) { height = height.integerValue + "px" } if (IS(width)) { width = width.integerValue + "px" } //let str = element.style.cssText frameTransform = "translate3d(" + (left).integerValue + "px, " + (top).integerValue + "px, 0px)" + frameTransform const style = element.style if (element.UIView) { frameTransform = frameTransform + (style.transform.match( new RegExp("scale\\(.*\\)", "g") )?.firstElement ?? "") //str = str + UIView._transformAttribute + ": " + frameTransform + ";" } if (IS_NIL(height)) { //str = str + " height: unset;" height = "unset" } // else { // str = str + " height:" + height + ";" // } if (IS_NIL(width)) { //str = str + " width: unset;" width = "unset" } // else { // str = str + " width:" + width + ";" // } let zIndexString = "" + zIndex if (IS_NIL(zIndex)) { //str = str + " z-index: unset;" zIndexString = "unset" } // else { // str = str + " z-index:" + zIndex + ";" // } style.transform = frameTransform style.height = height style.width = width style.zIndex = zIndexString //element.style.cssText = element.style.cssText + str } static performAutoLayout( parentElement: HTMLElement & LooseObject, visualFormatArray: string | any[] | null, constraintsArray: string | any[] ) { const view = new AutoLayout.View() if (IS(visualFormatArray) && IS(visualFormatArray.length)) { view.addConstraints(AutoLayout.VisualFormat.parse(visualFormatArray, { extended: true })) } if (IS(constraintsArray) && IS(constraintsArray.length)) { view.addConstraints(constraintsArray) } const elements: Record<string, HTMLElement> = {} for (var key in view.subViews) { if (!view.subViews.hasOwnProperty(key)) { continue } var element = nil try { element = parentElement.querySelector("#" + key) } catch (error) { //console.log("Error occurred " + error); } if (!(element && !element.obeyAutolayout && !element.getAttribute("obeyAutolayout")) && element) { element.className += element.className ? " abs" : "abs" elements[key] = element } } let parentUIView = nil if (parentElement.UIView) { parentUIView = parentElement.UIView } const updateLayout = function () { view.setSize( parentElement ? parentElement.clientWidth : window.innerWidth, parentElement ? parentElement.clientHeight : window.innerHeight ) for (key in view.subViews) { if (!view.subViews.hasOwnProperty(key)) { continue } const subView = view.subViews[key] if (elements[key]) { UIView._setAbsoluteSizeAndPosition( elements[key], subView.left, subView.top, subView.width, subView.height, // @ts-ignore elements[key].UIView.frame.zIndex, // @ts-ignore elements[key].UIView.frameTransform ) } } parentUIView.didLayoutSubviews() } updateLayout() return updateLayout } static runFunctionBeforeNextFrame(step: () => void) { if (IS_SAFARI) { // This creates a microtask Promise.resolve().then(step) } else { window.requestAnimationFrame(step) } } static scheduleLayoutViewsIfNeeded() { UIView.runFunctionBeforeNextFrame(UIView.layoutViewsIfNeeded) } static layoutViewsIfNeeded() { for (var i = 0; i < UIView._viewsToLayout.length; i++) { const view = UIView._viewsToLayout[i] view.layoutIfNeeded() } UIView._viewsToLayout = [] } setNeedsLayout() { if (this._shouldLayout) { return } this._shouldLayout = YES // Register view for layout before next frame UIView._viewsToLayout.push(this) this._intrinsicSizesCache = {} if (UIView._viewsToLayout.length == 1) { UIView.scheduleLayoutViewsIfNeeded() } } get needsLayout() { return this._shouldLayout } layoutIfNeeded() { if (!this._shouldLayout) { return } this._shouldLayout = NO try { this.layoutSubviews() } catch (exception) { console.log(exception) } } layoutSubviews() { this.willLayoutSubviews() this._shouldLayout = NO // Autolayout if (this.constraints.length) { this._updateLayoutFunction = UIView.performAutoLayout(this.viewHTMLElement, null, this.constraints) } this._updateLayoutFunction?.() this.viewController?.layoutViewSubviews() this.applyClassesAndStyles() for (let i = 0; i < this.subviews.length; i++) { const subview = this.subviews[i] subview.calculateAndSetViewFrame() } this.didLayoutSubviews() } applyClassesAndStyles() { for (let i = 0; i < this.styleClasses.length; i++) { const styleClass = this.styleClasses[i] if (styleClass && !this.viewHTMLElement.classList.contains(styleClass)) { this.viewHTMLElement.classList.add(styleClass) } } } willLayoutSubviews() { this.viewController?.viewWillLayoutSubviews() } didLayoutSubviews() { this.viewController?.viewDidLayoutSubviews() } get constraints() { return th