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
710 lines (398 loc) • 19.3 kB
text/typescript
import { UIButton } from "./UIButton"
import { UINativeScrollView } from "./UINativeScrollView"
import { FIRST_OR_NIL, IS, IS_DEFINED, nil, NO, YES } from "./UIObject"
import { UIPoint } from "./UIPoint"
import { UIRectangle } from "./UIRectangle"
import { UIView, UIViewBroadcastEvent } from "./UIView"
interface UITableViewRowView extends UIView {
_UITableViewRowIndex?: number;
}
export interface UITableViewReusableViewsContainerObject {
[key: string]: UIView[];
}
export interface UITableViewReusableViewPositionObject {
bottomY: number;
topY: number;
isValid: boolean;
}
export class UITableView extends UINativeScrollView {
allRowsHaveEqualHeight: boolean = NO
_visibleRows: UITableViewRowView[] = []
_firstLayoutVisibleRows: UITableViewRowView[] = []
_rowPositions: UITableViewReusableViewPositionObject[] = []
_highestValidRowPositionIndex: number = 0
_reusableViews: UITableViewReusableViewsContainerObject = {}
_removedReusableViews: UITableViewReusableViewsContainerObject = {}
_fullHeightView: UIView
_rowIDIndex: number = 0
reloadsOnLanguageChange = YES
sidePadding = 0
cellWeights?: number[]
_persistedData: any[] = []
_needsDrawingOfVisibleRowsBeforeLayout = NO
_isDrawVisibleRowsScheduled = NO
_shouldAnimateNextLayout?: boolean
override animationDuration = 0.25
constructor(elementID?: string) {
super(elementID)
this._fullHeightView = new UIView()
this._fullHeightView.hidden = YES
this._fullHeightView.userInteractionEnabled = NO
this.addSubview(this._fullHeightView)
this.scrollsX = NO
}
loadData() {
this._persistedData = []
this._calculatePositionsUntilIndex(this.numberOfRows() - 1)
this._needsDrawingOfVisibleRowsBeforeLayout = YES
this.setNeedsLayout()
}
reloadData() {
this._removeVisibleRows()
this._removeAllReusableRows()
this._rowPositions = []
this._highestValidRowPositionIndex = 0
this.loadData()
}
highlightChanges(previousData: any[], newData: any[]) {
previousData = previousData.map(dataPoint => JSON.stringify(dataPoint))
newData = newData.map(dataPoint => JSON.stringify(dataPoint))
const newIndexes: number[] = []
newData.forEach((value, index) => {
if (!previousData.contains(value)) {
newIndexes.push(index)
}
})
newIndexes.forEach(index => {
if (this.isRowWithIndexVisible(index)) {
this.highlightRowAsNew(this.visibleRowWithIndex(index) as UIView)
}
})
}
highlightRowAsNew(row: UIView) {
}
invalidateSizeOfRowWithIndex(index: number, animateChange = NO) {
if (this._rowPositions[index]) {
this._rowPositions[index].isValid = NO
}
this._highestValidRowPositionIndex = Math.min(this._highestValidRowPositionIndex, index - 1)
// if (index == 0) {
// this._highestValidRowPositionIndex = 0;
// this._rowPositions = [];
// }
this._needsDrawingOfVisibleRowsBeforeLayout = YES
this._shouldAnimateNextLayout = animateChange
}
_calculateAllPositions() {
this._calculatePositionsUntilIndex(this.numberOfRows() - 1)
}
_calculatePositionsUntilIndex(maxIndex: number) {
var validPositionObject = this._rowPositions[this._highestValidRowPositionIndex]
if (!IS(validPositionObject)) {
validPositionObject = {
bottomY: 0,
topY: 0,
isValid: YES
}
}
var previousBottomY = validPositionObject.bottomY
if (!this._rowPositions.length) {
this._highestValidRowPositionIndex = -1
}
for (var i = this._highestValidRowPositionIndex + 1; i <= maxIndex; i++) {
var height: number
const rowPositionObject = this._rowPositions[i]
if (IS((rowPositionObject || nil).isValid)) {
height = rowPositionObject.bottomY - rowPositionObject.topY
}
else {
height = this.heightForRowWithIndex(i)
}
const positionObject: UITableViewReusableViewPositionObject = {
bottomY: previousBottomY + height,
topY: previousBottomY,
isValid: YES
}
if (i < this._rowPositions.length) {
this._rowPositions[i] = positionObject
}
else {
this._rowPositions.push(positionObject)
}
this._highestValidRowPositionIndex = i
previousBottomY = previousBottomY + height
}
}
indexesForVisibleRows(paddingRatio = 0.5): number[] {
const firstVisibleY = this.contentOffset.y - this.bounds.height * paddingRatio
const lastVisibleY = firstVisibleY + this.bounds.height * (1 + paddingRatio)
const numberOfRows = this.numberOfRows()
if (this.allRowsHaveEqualHeight) {
const rowHeight = this.heightForRowWithIndex(0)
var firstIndex = firstVisibleY / rowHeight
var lastIndex = lastVisibleY / rowHeight
firstIndex = Math.trunc(firstIndex)
lastIndex = Math.trunc(lastIndex) + 1
firstIndex = Math.max(firstIndex, 0)
lastIndex = Math.min(lastIndex, numberOfRows - 1)
var result = []
for (var i = firstIndex; i < lastIndex + 1; i++) {
result.push(i)
}
return result
}
var accumulatedHeight = 0
var result = []
this._calculateAllPositions()
const rowPositions = this._rowPositions
for (var i = 0; i < numberOfRows; i++) {
const height = rowPositions[i].bottomY - rowPositions[i].topY // this.heightForRowWithIndex(i)
accumulatedHeight = accumulatedHeight + height
if (accumulatedHeight >= firstVisibleY) {
result.push(i)
}
if (accumulatedHeight >= lastVisibleY) {
break
}
}
return result
}
_removeVisibleRows() {
const visibleRows: UITableViewRowView[] = []
this._visibleRows.forEach((row: UIView) => {
this._persistedData[row._UITableViewRowIndex as number] = this.persistenceDataItemForRowWithIndex(
row._UITableViewRowIndex as number,
row
)
row.removeFromSuperview()
this._removedReusableViews[row?._UITableViewReusabilityIdentifier]?.push(row)
})
this._visibleRows = visibleRows
}
_removeAllReusableRows() {
this._reusableViews.forEach((rows: UIView[]) =>
rows.forEach((row: UIView) => {
this._persistedData[row._UITableViewRowIndex as number] = this.persistenceDataItemForRowWithIndex(
row._UITableViewRowIndex as number,
row
)
row.removeFromSuperview()
this._markReusableViewAsUnused(row)
})
)
}
_markReusableViewAsUnused(row: UIView) {
if (!this._removedReusableViews[row._UITableViewReusabilityIdentifier].contains(row)) {
this._removedReusableViews[row._UITableViewReusabilityIdentifier].push(row)
}
}
_drawVisibleRows() {
if (!this.isMemberOfViewTree) {
return
}
const visibleIndexes = this.indexesForVisibleRows()
const minIndex = visibleIndexes[0]
const maxIndex = visibleIndexes[visibleIndexes.length - 1]
const removedViews: UITableViewRowView[] = []
const visibleRows: UITableViewRowView[] = []
this._visibleRows.forEach((row) => {
if (IS_DEFINED(row._UITableViewRowIndex) && (row._UITableViewRowIndex < minIndex || row._UITableViewRowIndex > maxIndex)) {
//row.removeFromSuperview();
this._persistedData[row._UITableViewRowIndex] = this.persistenceDataItemForRowWithIndex(
row._UITableViewRowIndex,
row
)
this._removedReusableViews[row._UITableViewReusabilityIdentifier].push(row)
removedViews.push(row)
}
else {
visibleRows.push(row)
}
})
this._visibleRows = visibleRows
visibleIndexes.forEach((rowIndex: number) => {
if (this.isRowWithIndexVisible(rowIndex)) {
return
}
const view: UITableViewRowView = this.viewForRowWithIndex(rowIndex)
//view._UITableViewRowIndex = rowIndex;
this._firstLayoutVisibleRows.push(view)
this._visibleRows.push(view)
this.addSubview(view)
})
for (let i = 0; i < removedViews.length; i++) {
const view = removedViews[i]
if (this._visibleRows.indexOf(view) == -1) {
//this._persistedData[view._UITableViewRowIndex] = this.persistenceDataItemForRowWithIndex(view._UITableViewRowIndex, view);
view.removeFromSuperview()
//this._removedReusableViews[view._UITableViewReusabilityIdentifier].push(view);
}
}
//this.setNeedsLayout();
}
visibleRowWithIndex(rowIndex: number | undefined): UIView {
for (var i = 0; i < this._visibleRows.length; i++) {
const row = this._visibleRows[i]
if (row._UITableViewRowIndex == rowIndex) {
return row
}
}
return nil
}
isRowWithIndexVisible(rowIndex: number) {
return IS(this.visibleRowWithIndex(rowIndex))
}
reusableViewForIdentifier(identifier: string, rowIndex: number): UITableViewRowView {
if (!this._removedReusableViews[identifier]) {
this._removedReusableViews[identifier] = []
}
if (this._removedReusableViews[identifier] && this._removedReusableViews[identifier].length) {
const view = this._removedReusableViews[identifier].pop() as UITableViewRowView
view._UITableViewRowIndex = rowIndex
Object.assign(view, this._persistedData[rowIndex] || this.defaultRowPersistenceDataItem())
return view
}
if (!this._reusableViews[identifier]) {
this._reusableViews[identifier] = []
}
const newView = this.newReusableViewForIdentifier(identifier, this._rowIDIndex) as UITableViewRowView
this._rowIDIndex = this._rowIDIndex + 1
newView._UITableViewReusabilityIdentifier = identifier
newView._UITableViewRowIndex = rowIndex
Object.assign(newView, this._persistedData[rowIndex] || this.defaultRowPersistenceDataItem())
this._reusableViews[identifier].push(newView)
return newView
}
// Functions that should be overridden to draw the correct content START
newReusableViewForIdentifier(identifier: string, rowIDIndex: number): UIView {
const view = new UIButton(this.elementID + "Row" + rowIDIndex)
view.stopsPointerEventPropagation = NO
view.pausesPointerEvents = NO
return view
}
heightForRowWithIndex(index: number): number {
return 50
}
numberOfRows() {
return 10000
}
defaultRowPersistenceDataItem(): any {
}
persistenceDataItemForRowWithIndex(rowIndex: number, row: UIView): any {
}
viewForRowWithIndex(rowIndex: number): UITableViewRowView {
const row = this.reusableViewForIdentifier("Row", rowIndex)
row._UITableViewRowIndex = rowIndex
FIRST_OR_NIL((row as unknown as UIButton).titleLabel).text = "Row " + rowIndex
return row
}
// Functions that should be overridden to draw the correct content END
// Functions that trigger redrawing of the content
override didScrollToPosition(offsetPosition: UIPoint) {
super.didScrollToPosition(offsetPosition)
this.forEachViewInSubtree(function (view: UIView) {
view._isPointerValid = NO
})
if (!this._isDrawVisibleRowsScheduled) {
this._isDrawVisibleRowsScheduled = YES
UIView.runFunctionBeforeNextFrame(function (this: UITableView) {
this._calculateAllPositions()
this._drawVisibleRows()
this.setNeedsLayout()
this._isDrawVisibleRowsScheduled = NO
}.bind(this))
}
}
override wasAddedToViewTree() {
this.loadData()
}
override setFrame(rectangle: UIRectangle, zIndex?: number, performUncheckedLayout?: boolean) {
const frame = this.frame
super.setFrame(rectangle, zIndex, performUncheckedLayout)
if (frame.isEqualTo(rectangle) && !performUncheckedLayout) {
return
}
this._needsDrawingOfVisibleRowsBeforeLayout = YES
}
override didReceiveBroadcastEvent(event: UIViewBroadcastEvent) {
super.didReceiveBroadcastEvent(event)
if (event.name == UIView.broadcastEventName.LanguageChanged && this.reloadsOnLanguageChange) {
this.reloadData()
}
}
private _layoutAllRows(positions = this._rowPositions) {
const bounds = this.bounds
this._visibleRows.sort((rowA, rowB) => rowA._UITableViewRowIndex! - rowB._UITableViewRowIndex!)
.forEach(row => {
const frame = bounds.copy()
const positionObject = positions[row._UITableViewRowIndex!]
frame.min.y = positionObject.topY
frame.max.y = positionObject.bottomY
row.frame = frame
row.style.width = "" + (bounds.width - this.sidePadding * 2).integerValue + "px"
row.style.left = "" + this.sidePadding.integerValue + "px"
// This is to reorder the elements in the DOM
this.viewHTMLElement.appendChild(row.viewHTMLElement)
})
this._fullHeightView.frame = bounds.rectangleWithHeight((positions.lastElement ||
nil).bottomY).rectangleWithWidth(bounds.width * 0.5)
this._firstLayoutVisibleRows = []
}
private _animateLayoutAllRows() {
UIView.animateViewOrViewsWithDurationDelayAndFunction(
this._visibleRows,
this.animationDuration,
0,
undefined,
function (this: UITableView) {
this._layoutAllRows()
}.bind(this),
function (this: UITableView) {
// this._calculateAllPositions()
// this._layoutAllRows()
}.bind(this)
)
}
override layoutSubviews() {
const previousPositions: UITableViewReusableViewPositionObject[] = JSON.parse(JSON.stringify(this._rowPositions))
const previousVisibleRowsLength = this._visibleRows.length
if (this._needsDrawingOfVisibleRowsBeforeLayout) {
//this._calculateAllPositions()
this._drawVisibleRows()
this._needsDrawingOfVisibleRowsBeforeLayout = NO
}
super.layoutSubviews()
if (!this.numberOfRows() || !this.isMemberOfViewTree) {
return
}
if (this._shouldAnimateNextLayout) {
// Need to do layout with the previous positions
this._layoutAllRows(previousPositions)
if (previousVisibleRowsLength < this._visibleRows.length) {
UIView.runFunctionBeforeNextFrame(function (this: UITableView) {
this._animateLayoutAllRows()
}.bind(this))
}
else {
this._animateLayoutAllRows()
}
this._shouldAnimateNextLayout = NO
}
else {
// if (this._needsDrawingOfVisibleRowsBeforeLayout) {
// this._drawVisibleRows();
// this._needsDrawingOfVisibleRowsBeforeLayout = NO;
// }
this._calculateAllPositions()
this._layoutAllRows()
}
}
override intrinsicContentHeight(constrainingWidth = 0) {
var result = 0
this._calculateAllPositions()
if (this._rowPositions.length) {
result = this._rowPositions[this._rowPositions.length - 1].bottomY
}
return result
}
}