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
text/typescript
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