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,651 lines (1,230 loc) • 150 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 { UILayoutCycleTracer } from "./UILayoutCycleTracer"
import { UILayoutDebugger } from "./UILayoutDebugger"
import {
FIRST,
FIRST_OR_NIL,
IF,
IS,
IS_DEFINED,
IS_NIL,
IS_NOT,
IS_NOT_LIKE_NULL,
nil,
NO,
RETURNER, UIInitializerObject,
UIObject,
YES
} from "./UIObject"
import { UIPoint } from "./UIPoint"
import { UIRectangle } from "./UIRectangle"
import { UITextMeasurement } from "./UITextMeasurement"
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]
};
type ColorStyleProxy = {
[K in keyof CSSStyleDeclaration as K extends string
? (Lowercase<K> extends `${string}color${string}` ? K : never)
: never]?: UIColor
}
export type UIViewAddControlEventTargetObject<T extends { controlEvent: Record<string, any> }> = Mutable<{
-readonly [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 interface IUILoadingView extends UIView {
theme: "light" | "dark";
// Add any other specific methods you need to call from UIView
}
/**
* A template literal tag to enable CSS highlighting in IDEs.
* Example: css` .myClass { color: red; } `
*/
export function css(strings: TemplateStringsArray, ...values: any[]): string {
// Simply combine the strings and values to return a valid CSS string
return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ""), "")
}
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 }
_frameCache?: UIRectangle
_backgroundColor: UIColor = nil
_colorStyleProxy?: ColorStyleProxy
_liveCSSValues: Map<string, { keys: string[], producer: () => string }> = new Map()
_liveCSSCallback: () => void = () => {
for (const [property, { producer }] of this._liveCSSValues) {
(this.style as any)[property] = producer()
}
}
_overlayView?: UIView
get overlayView(): UIView | undefined {
return this._overlayView
}
set overlayView(view: UIView) {
if (IS(view)) {
this.addSubview(view)
}
else {
this._overlayView?.removeFromSuperview()
}
this._overlayView = view
}
_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[]; };
_broadcastEventTargets: { [eventName: string]: ((event: UIViewBroadcastEvent) => void)[] } = {}
_ongoingControlEventHandlers: { _controlEventKey: string }[] = []
_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
// When the page's fonts finish loading the browser's glyph metrics change,
// making all previously cached text measurements stale. We hook the native
// FontFaceSet.ready promise once at class-load time. When it fires we:
// 1. Clear all UITextMeasurement style caches (stale fallback-font metrics)
// 2. Clear every view's intrinsic size cache and frame cache
// 3. Force a full re-layout, mirroring what the window resize handler does
// document.fonts.ready resolves once all fonts have loaded, but the browser
// may not yet have applied the new glyphs to rendered elements at that point.
// A requestAnimationFrame defers the cache-bust and re-layout by one paint
// cycle, ensuring getComputedStyle and the canvas measurer see real metrics.
static _fontReadyHook = (() => {
document.fonts.ready.then(() => {
requestAnimationFrame(() => {
UITextMeasurement.clearCaches()
const rootView = UICore.main?.rootViewController?.view
if (!rootView) {
return
}
rootView.forEachViewInSubtree(view => {
view.clearIntrinsicSizeCache()
view._frameCache = undefined
view.documentFontsDidLoad()
view.setNeedsLayout()
})
UIView.layoutViewsIfNeeded()
})
})
return 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[] = []
public static LoadingViewClass: new () => IUILoadingView
private _loadingView?: IUILoadingView
public set loading(isLoading: boolean) {
this.userInteractionEnabled = !isLoading
if (isLoading) {
if (!IS(this._loadingView)) {
if (UIView.LoadingViewClass) {
this._loadingView = new UIView.LoadingViewClass()
}
else {
console.warn("UILoadingView class not registered yet.")
return
}
}
// Add to superview if not already added
if (this._loadingView.superview != this) {
this.addSubview(this._loadingView)
// Ensure it sits above other content
this._loadingView.style.zIndex = "1000"
}
// Force an immediate layout update to position the overlay
// this._loadingView.setFrame(this.bounds)
}
else {
this._loadingView?.removeFromSuperview()
}
}
public get loading(): boolean {
return IS(this._loadingView) && IS(this._loadingView.superview)
}
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
protected _intrinsicSizesCache: Record<string, UIRectangle> = {}
/** When set, this view reads and writes its intrinsic size cache entries from the
* static shared map keyed by this identifier rather than from its own per-instance
* cache. All views that share the same identifier share the same cached sizes, so
* only the first measurement in a group is ever computed.
*
* Invalidate with UIView.invalidateSharedIntrinsicSizeCache(identifier). */
sharedIntrinsicSizeCacheIdentifier?: string
/** Static shared intrinsic size cache, keyed by sharedIntrinsicSizeCacheIdentifier.
* Each bucket mirrors the per-instance _intrinsicSizesCache format. */
static _sharedIntrinsicSizeCaches: Map<string, Record<string, UIRectangle>> = new Map()
static invalidateSharedIntrinsicSizeCache(identifier: string): void {
UIView._sharedIntrinsicSizeCaches.delete(identifier)
}
static invalidateAllSharedIntrinsicSizeCaches(): void {
UIView._sharedIntrinsicSizeCaches.clear()
}
private static _virtualLayoutingDepth = 0
static get isVirtualLayouting(): boolean {
return this._virtualLayoutingDepth > 0
}
get isVirtualLayouting(): boolean {
return UIView._virtualLayoutingDepth > 0
}
startVirtualLayout() {
UIView._virtualLayoutingDepth = UIView._virtualLayoutingDepth + 1
}
finishVirtualLayout() {
if (UIView._virtualLayoutingDepth === 0) {
throw new Error("Unbalanced finishVirtualLayout()")
}
UIView._virtualLayoutingDepth = UIView._virtualLayoutingDepth - 1
}
_frameForVirtualLayouting?: UIRectangle
_frameCacheForVirtualLayouting?: UIRectangle
// Change this to no if the view contains pure HTML content that does not
// use frame logic that can influence the intrinsic size
usesVirtualLayoutingForIntrinsicSizing = YES
_contentInsets = { top: 0, left: 0, bottom: 0, right: 0 }
get contentInsets() {
return this._contentInsets
}
set contentInsets(insets: { left: number, right: number, bottom: number, top: number }) {
this._contentInsets = insets
this.setNeedsLayout()
}
constructor(
elementID: string = ("UIView" + UIView.nextIndex),
viewHTMLElement: HTMLElement & LooseObject | null = null,
elementType: string | null = null,
preInitConfiguratorObject?: any
) {
super()
this.configureWithObject(preInitConfiguratorObject)
// 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)
// }
//#region Static Properties - Page Dimensions Cache
private static _cachedPageWidth: number | undefined
private static _cachedPageHeight: number | undefined
private static _pageDimensionsCacheValid = false
private static _resizeObserverInitialized = false
//#endregion
//#region Static Methods - Page Dimensions
/**
* Initialize resize observer to invalidate cache when page dimensions change.
* This is called lazily on first access.
*/
private static _initializePageDimensionsCacheIfNeeded() {
if (this._resizeObserverInitialized) {
return
}
this._resizeObserverInitialized = true
// Invalidate cache on window resize
window.addEventListener("resize", () => {
this._pageDimensionsCacheValid = false
}, { passive: true })
// Observe document.body for mutations that might affect dimensions
const bodyObserver = new ResizeObserver(() => {
this._pageDimensionsCacheValid = false
})
// Start observing once body is available
if (document.body) {
bodyObserver.observe(document.body)
}
else {
// Wait for DOMContentLoaded if body isn't ready yet
document.addEventListener("DOMContentLoaded", () => {
bodyObserver.observe(document.body)
}, { once: true })
}
// Also invalidate on DOM mutations that might add/remove content
const mutationObserver = new MutationObserver(() => {
this._pageDimensionsCacheValid = false
})
const observeMutations = () => {
mutationObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style", "class"]
})
}
if (document.body) {
observeMutations()
}
else {
document.addEventListener("DOMContentLoaded", observeMutations, { once: true })
}
}
/**
* Compute and cache page dimensions.
* Only triggers reflow when cache is invalid.
*/
private static _updatePageDimensionsCacheIfNeeded() {
if (this._pageDimensionsCacheValid &&
this._cachedPageWidth !== undefined &&
this._cachedPageHeight !== undefined) {
return
}
const body = document.body
const html = document.documentElement
// Compute both at once to minimize reflows
this._cachedPageWidth = Math.max(
body.scrollWidth,
body.offsetWidth,
html.clientWidth,
html.scrollWidth,
html.offsetWidth
)
this._cachedPageHeight = Math.max(
body.scrollHeight,
body.offsetHeight,
html.clientHeight,
html.scrollHeight,
html.offsetHeight
)
this._pageDimensionsCacheValid = true
}
static get pageWidth() {
this._initializePageDimensionsCacheIfNeeded()
this._updatePageDimensionsCacheIfNeeded()
return this._cachedPageWidth!
}
static get pageHeight() {
this._initializePageDimensionsCacheIfNeeded()
this._updatePageDimensionsCacheIfNeeded()
return this._cachedPageHeight!
}
/**
* Manually invalidate the page dimensions cache.
* Useful if you know dimensions changed and want to force a recalculation.
*/
static invalidatePageDimensionsCache() {
this._pageDimensionsCacheValid = false
}
//#endregion
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 | undefined | null) {
if (IS_NOT_LIKE_NULL(hoverText)) {
this.viewHTMLElement.setAttribute("title", hoverText)
}
else {
this.viewHTMLElement.removeAttribute("title")
}
}
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 injectCSS(cssText: string, id?: string) {
if (id) {
const existing = document.getElementById(id) as HTMLStyleElement | null
if (existing) {
if (existing.textContent !== cssText) {
existing.textContent = cssText
}
return
}
}
const style = document.createElement("style")
if (id) {
style.id = id
}
style.textContent = cssText
document.head.appendChild(style)
}
static createStyleSelector(selector: string, style: string) {
this.injectCSS(selector + " { " + style + " }")
}
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 }
if (!this.isVirtualLayouting) {
result = this._frame?.copy() as any
}
else {
result = this._frameForVirtualLayouting?.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)
}
}
// If YES, then the view is not counted in intrinsic content size calculation.
// This should be used for things like background views that just take the shape of the parent view.
hasWeakFrame = NO
// Set view as having a weak frame and set the frame.
public set weakFrame(rectangle: UIRectangle & { zIndex?: number }) {
this.hasWeakFrame = YES
this.frame = rectangle
}
// Set view as having a strong frame and set the frame.
public set strongFrame(rectangle: UIRectangle & { zIndex?: number }) {
this.hasWeakFrame = NO
this.frame = rectangle
}
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 (!rectangle) {
return
}
rectangle.materialize()
if (zIndex != undefined) {
rectangle.zIndex = zIndex
}
if (!this.isVirtualLayouting) {
this._frame = rectangle
}
else {
this._frameForVirtualLayouting = rectangle
}
if (frame && frame != nil && frame.isEqualTo(
rectangle) && frame.zIndex == rectangle.zIndex && !performUncheckedLayout) {
return
}
if (this.isInternalScaling) {
rectangle.scale(1 / this.scale)
}
if (!this.isVirtualLayouting) {
UIView._setAbsoluteSizeAndPosition(
this.viewHTMLElement,
rectangle.topLeft.x,
rectangle.topLeft.y,
rectangle.width,
rectangle.height,
rectangle.zIndex,
this.frameTransform
)
}
const haveBoundsChanged = (frame.height.integerValue != rectangle.height.integerValue) ||
(frame.width.integerValue != rectangle.width.integerValue)
if (haveBoundsChanged || performUncheckedLayout) {
this.setNeedsLayout()
this.boundsDidChange(this.bounds)
}
}
get bounds() {
let _frame: (UIRectangle & { zIndex?: number }) | undefined
if (!this.isVirtualLayouting) {
_frame = this._frame
}
else {
_frame = this._frameForVirtualLayouting
}
let result: UIRectangle
if (IS_NOT(_frame)) {
let cachedFrame: UIRectangle | undefined
if (!this.isVirtualLayouting) {
cachedFrame = this._frameCache
}
else {
cachedFrame = this._frameCacheForVirtualLayouting
}
result = cachedFrame ?? new UIRectangle(
0,
0,
this._resizeObserverEntry?.contentRect.height ?? this.viewHTMLElement.offsetHeight,
this._resizeObserverEntry?.contentRect.width ?? this.viewHTMLElement.offsetWidth
)
if (!this.isVirtualLayouting && this.isMemberOfViewTree && this.viewHTMLElement.isConnected) {
this._frameCache = result
}
else if (this.isMemberOfViewTree && this.viewHTMLElement.isConnected) {
this._frameCacheForVirtualLayouting = result
}
}
else {
let frame: (UIRectangle & { zIndex?: number })
if (!this.isVirtualLayouting) {
frame = this.frame
}
else {
frame = this._frameForVirtualLayouting ?? this.frame
}
result = frame.copy()
result.x = 0
result.y = 0
}
result.minHeight = 0
result.minWidth = 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._frameCache = undefined
this._frameCacheForVirtualLayouting = undefined
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.colorStyleProxy.borderColor = color
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) {
const previous = this._backgroundColor
if (previous === backgroundColor) {
return
}
if (
previous?.semanticKey &&
previous.semanticKey === backgroundColor?.semanticKey &&
previous._semanticClass === backgroundColor?._semanticClass
) {
return
}
if (!previous?.semanticKey && !backgroundColor?.semanticKey &&
previous?.stringValue === backgroundColor?.stringValue) {
return
}
this._backgroundColor = backgroundColor
this.colorStyleProxy.backgroundColor = backgroundColor
}
get colorStyleProxy(): ColorStyleProxy {
if (!this._colorStyleProxy) {
const element = this._viewHTMLElement
this._colorStyleProxy = new Proxy(element.style, {
set: (target, property, value: UIColor) => {
const registrationKey = `${this._UIViewIndex}_${property as string}`
const previousColor = UIColor._registrationMap.get(registrationKey)
if (previousColor) {
previousColor._elementRef = undefined
previousColor._styleProperty = undefined
}
value._elementRef = element
value._styleProperty = property as string
if (value.semanticKey) {
UIColor._liveColors.push(new WeakRef(value))
}
UIColor._registrationMap.set(registrationKey, value)
Reflect.set(target, property, value.stringValue)
return true
}
}) as unknown as ColorStyleProxy
}
return this._colorStyleProxy
}
/**
* Registers a producer function that generates a composite CSS string value
* (e.g. a gradient or box-shadow with embedded colors) and re-runs it
* automatically whenever any of the given semantic keys are updated via
* applySemanticColors(). Replaces any previous registration for the same
* CSS property. The initial value is written immediately.
*/
registerLiveCSSValue(property: string, keys: string[], producer: () => string) {
const previous = this._liveCSSValues.get(property)
if (previous) {
for (const key of previous.keys) {