mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-15 11:01:21 +08:00

* chore: move tns-core-modules to nativescript-core * chore: preparing compat generate script * chore: add missing definitions * chore: no need for http-request to be private * chore: packages chore * test: generate tests for tns-core-modules * chore: add anroid module for consistency * chore: add .npmignore * chore: added privateModulesWhitelist * chore(webpack): added bundle-entry-points * chore: scripts * chore: tests changed to use @ns/core * test: add scoped-packages test project * test: fix types * test: update test project * chore: build scripts * chore: update build script * chore: npm scripts cleanup * chore: make the compat pgk work with old wp config * test: generate diff friendly tests * chore: create barrel exports * chore: move files after rebase * chore: typedoc config * chore: compat mode * chore: review of barrels * chore: remove tns-core-modules import after rebase * chore: dev workflow setup * chore: update developer-workflow * docs: experiment with API extractor * chore: api-extractor and barrel exports * chore: api-extractor configs * chore: generate d.ts rollup with api-extractor * refactor: move methods inside Frame * chore: fic tests to use Frame static methods * refactor: create Builder class * refactor: use Builder class in tests * refactor: include Style in ui barrel * chore: separate compat build script * chore: fix tslint errors * chore: update NATIVESCRIPT_CORE_ARGS * chore: fix compat pack * chore: fix ui-test-app build with linked modules * chore: Application, ApplicationSettings, Connectivity and Http * chore: export Trace, Profiling and Utils * refactor: Static create methods for ImageSource * chore: fix deprecated usages of ImageSource * chore: move Span and FormattedString to ui * chore: add events-args and ImageSource to index files * chore: check for CLI >= 6.2 when building for IOS * chore: update travis build * chore: copy Pod file to compat package * chore: update error msg ui-tests-app * refactor: Apply suggestions from code review Co-Authored-By: Martin Yankov <m.i.yankov@gmail.com> * chore: typings and refs * chore: add missing d.ts files for public API * chore: adress code review FB * chore: update api-report * chore: dev-workflow for other apps * chore: api update * chore: update api-report
517 lines
18 KiB
TypeScript
517 lines
18 KiB
TypeScript
import { ItemEventData } from ".";
|
|
import {
|
|
ListViewBase, View, KeyedTemplate, Length, Observable, Color,
|
|
separatorColorProperty, itemTemplatesProperty, iosEstimatedRowHeightProperty, layout, EventData
|
|
} from "./list-view-common";
|
|
import { StackLayout } from "../layouts/stack-layout";
|
|
import { ProxyViewContainer } from "../proxy-view-container";
|
|
import { profile } from "../../profiling";
|
|
import * as trace from "../../trace";
|
|
|
|
export * from "./list-view-common";
|
|
|
|
const ITEMLOADING = ListViewBase.itemLoadingEvent;
|
|
const LOADMOREITEMS = ListViewBase.loadMoreItemsEvent;
|
|
const ITEMTAP = ListViewBase.itemTapEvent;
|
|
const DEFAULT_HEIGHT = 44;
|
|
|
|
const infinity = layout.makeMeasureSpec(0, layout.UNSPECIFIED);
|
|
|
|
interface ViewItemIndex {
|
|
_listViewItemIndex?: number;
|
|
}
|
|
|
|
type ItemView = View & ViewItemIndex;
|
|
|
|
class ListViewCell extends UITableViewCell {
|
|
public static initWithEmptyBackground(): ListViewCell {
|
|
const cell = <ListViewCell>ListViewCell.new();
|
|
// Clear background by default - this will make cells transparent
|
|
cell.backgroundColor = null;
|
|
|
|
return cell;
|
|
}
|
|
|
|
initWithStyleReuseIdentifier(style: UITableViewCellStyle, reuseIdentifier: string): this {
|
|
const cell = <this>super.initWithStyleReuseIdentifier(style, reuseIdentifier);
|
|
// Clear background by default - this will make cells transparent
|
|
cell.backgroundColor = null;
|
|
|
|
return cell;
|
|
}
|
|
|
|
public willMoveToSuperview(newSuperview: UIView): void {
|
|
let parent = <ListView>(this.view ? this.view.parent : null);
|
|
|
|
// When inside ListView and there is no newSuperview this cell is
|
|
// removed from native visual tree so we remove it from our tree too.
|
|
if (parent && !newSuperview) {
|
|
parent._removeContainer(this);
|
|
}
|
|
}
|
|
|
|
public get view(): View {
|
|
return this.owner ? this.owner.get() : null;
|
|
}
|
|
|
|
public owner: WeakRef<View>;
|
|
}
|
|
|
|
function notifyForItemAtIndex(listView: ListViewBase, cell: any, view: View, eventName: string, indexPath: NSIndexPath) {
|
|
let args = <ItemEventData>{ eventName: eventName, object: listView, index: indexPath.row, view: view, ios: cell, android: undefined };
|
|
listView.notify(args);
|
|
|
|
return args;
|
|
}
|
|
|
|
class DataSource extends NSObject implements UITableViewDataSource {
|
|
public static ObjCProtocols = [UITableViewDataSource];
|
|
|
|
private _owner: WeakRef<ListView>;
|
|
|
|
public static initWithOwner(owner: WeakRef<ListView>): DataSource {
|
|
let dataSource = <DataSource>DataSource.new();
|
|
dataSource._owner = owner;
|
|
|
|
return dataSource;
|
|
}
|
|
|
|
public tableViewNumberOfRowsInSection(tableView: UITableView, section: number) {
|
|
let owner = this._owner.get();
|
|
|
|
return (owner && owner.items) ? owner.items.length : 0;
|
|
}
|
|
|
|
public tableViewCellForRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): UITableViewCell {
|
|
// We call this method because ...ForIndexPath calls tableViewHeightForRowAtIndexPath immediately (before we can prepare and measure it).
|
|
let owner = this._owner.get();
|
|
let cell: ListViewCell;
|
|
if (owner) {
|
|
let template = owner._getItemTemplate(indexPath.row);
|
|
cell = <ListViewCell>(tableView.dequeueReusableCellWithIdentifier(template.key) || ListViewCell.initWithEmptyBackground());
|
|
owner._prepareCell(cell, indexPath);
|
|
|
|
let cellView: View = cell.view;
|
|
if (cellView && cellView.isLayoutRequired) {
|
|
// Arrange cell views. We do it here instead of _layoutCell because _layoutCell is called
|
|
// from 'tableViewHeightForRowAtIndexPath' method too (in iOS 7.1) and we don't want to arrange the fake cell.
|
|
let width = layout.getMeasureSpecSize(owner.widthMeasureSpec);
|
|
let rowHeight = owner._effectiveRowHeight;
|
|
let cellHeight = rowHeight > 0 ? rowHeight : owner.getHeight(indexPath.row);
|
|
cellView.iosOverflowSafeAreaEnabled = false;
|
|
View.layoutChild(owner, cellView, 0, 0, width, cellHeight);
|
|
}
|
|
}
|
|
else {
|
|
cell = <ListViewCell>ListViewCell.initWithEmptyBackground();
|
|
}
|
|
|
|
return cell;
|
|
}
|
|
}
|
|
|
|
class UITableViewDelegateImpl extends NSObject implements UITableViewDelegate {
|
|
public static ObjCProtocols = [UITableViewDelegate];
|
|
|
|
private _owner: WeakRef<ListView>;
|
|
|
|
private _measureCellMap: Map<string, ListViewCell>;
|
|
|
|
public static initWithOwner(owner: WeakRef<ListView>): UITableViewDelegateImpl {
|
|
const delegate = <UITableViewDelegateImpl>UITableViewDelegateImpl.new();
|
|
delegate._owner = owner;
|
|
delegate._measureCellMap = new Map<string, ListViewCell>();
|
|
|
|
return delegate;
|
|
}
|
|
|
|
public tableViewWillDisplayCellForRowAtIndexPath(tableView: UITableView, cell: UITableViewCell, indexPath: NSIndexPath) {
|
|
const owner = this._owner.get();
|
|
if (owner && (indexPath.row === owner.items.length - 1)) {
|
|
owner.notify(<EventData>{ eventName: LOADMOREITEMS, object: owner });
|
|
}
|
|
}
|
|
|
|
public tableViewWillSelectRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): NSIndexPath {
|
|
const cell = <ListViewCell>tableView.cellForRowAtIndexPath(indexPath);
|
|
const owner = this._owner.get();
|
|
if (owner) {
|
|
notifyForItemAtIndex(owner, cell, cell.view, ITEMTAP, indexPath);
|
|
}
|
|
|
|
return indexPath;
|
|
}
|
|
|
|
public tableViewDidSelectRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): NSIndexPath {
|
|
tableView.deselectRowAtIndexPathAnimated(indexPath, true);
|
|
|
|
return indexPath;
|
|
}
|
|
|
|
public tableViewHeightForRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): number {
|
|
const owner = this._owner.get();
|
|
if (!owner) {
|
|
return tableView.estimatedRowHeight;
|
|
}
|
|
|
|
let height = owner.getHeight(indexPath.row);
|
|
if (height === undefined) {
|
|
// in iOS8+ after call to scrollToRowAtIndexPath:atScrollPosition:animated: this method is called before tableViewCellForRowAtIndexPath so we need fake cell to measure its content.
|
|
const template = owner._getItemTemplate(indexPath.row);
|
|
let cell = this._measureCellMap.get(template.key);
|
|
if (!cell) {
|
|
cell = (<any>tableView.dequeueReusableCellWithIdentifier(template.key)) || ListViewCell.initWithEmptyBackground();
|
|
this._measureCellMap.set(template.key, cell);
|
|
}
|
|
|
|
height = owner._prepareCell(cell, indexPath);
|
|
}
|
|
|
|
return layout.toDeviceIndependentPixels(height);
|
|
}
|
|
}
|
|
|
|
class UITableViewRowHeightDelegateImpl extends NSObject implements UITableViewDelegate {
|
|
public static ObjCProtocols = [UITableViewDelegate];
|
|
|
|
private _owner: WeakRef<ListView>;
|
|
|
|
public static initWithOwner(owner: WeakRef<ListView>): UITableViewRowHeightDelegateImpl {
|
|
let delegate = <UITableViewRowHeightDelegateImpl>UITableViewRowHeightDelegateImpl.new();
|
|
delegate._owner = owner;
|
|
|
|
return delegate;
|
|
}
|
|
|
|
public tableViewWillDisplayCellForRowAtIndexPath(tableView: UITableView, cell: UITableViewCell, indexPath: NSIndexPath) {
|
|
let owner = this._owner.get();
|
|
if (owner && (indexPath.row === owner.items.length - 1)) {
|
|
owner.notify(<EventData>{ eventName: LOADMOREITEMS, object: owner });
|
|
}
|
|
}
|
|
|
|
public tableViewWillSelectRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): NSIndexPath {
|
|
let cell = <ListViewCell>tableView.cellForRowAtIndexPath(indexPath);
|
|
let owner = this._owner.get();
|
|
if (owner) {
|
|
notifyForItemAtIndex(owner, cell, cell.view, ITEMTAP, indexPath);
|
|
}
|
|
|
|
return indexPath;
|
|
}
|
|
|
|
public tableViewDidSelectRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): NSIndexPath {
|
|
tableView.deselectRowAtIndexPathAnimated(indexPath, true);
|
|
|
|
return indexPath;
|
|
}
|
|
|
|
public tableViewHeightForRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): number {
|
|
let owner = this._owner.get();
|
|
if (!owner) {
|
|
return tableView.estimatedRowHeight;
|
|
}
|
|
|
|
return layout.toDeviceIndependentPixels(owner._effectiveRowHeight);
|
|
}
|
|
}
|
|
|
|
export class ListView extends ListViewBase {
|
|
public nativeViewProtected: UITableView;
|
|
// tslint:disable-next-line
|
|
private _dataSource;
|
|
private _delegate;
|
|
private _heights: Array<number>;
|
|
private _preparingCell: boolean;
|
|
private _isDataDirty: boolean;
|
|
private _map: Map<ListViewCell, ItemView>;
|
|
widthMeasureSpec: number = 0;
|
|
|
|
constructor() {
|
|
super();
|
|
this._map = new Map<ListViewCell, ItemView>();
|
|
this._heights = new Array<number>();
|
|
}
|
|
|
|
createNativeView() {
|
|
return UITableView.new();
|
|
}
|
|
|
|
initNativeView() {
|
|
super.initNativeView();
|
|
const nativeView = this.nativeViewProtected;
|
|
nativeView.registerClassForCellReuseIdentifier(ListViewCell.class(), this._defaultTemplate.key);
|
|
nativeView.estimatedRowHeight = DEFAULT_HEIGHT;
|
|
nativeView.rowHeight = UITableViewAutomaticDimension;
|
|
nativeView.dataSource = this._dataSource = DataSource.initWithOwner(new WeakRef(this));
|
|
this._delegate = UITableViewDelegateImpl.initWithOwner(new WeakRef(this));
|
|
this._setNativeClipToBounds();
|
|
}
|
|
|
|
disposeNativeView() {
|
|
this._delegate = null;
|
|
this._dataSource = null;
|
|
super.disposeNativeView();
|
|
}
|
|
|
|
_setNativeClipToBounds() {
|
|
// Always set clipsToBounds for list-view
|
|
this.ios.clipsToBounds = true;
|
|
}
|
|
|
|
@profile
|
|
public onLoaded() {
|
|
super.onLoaded();
|
|
if (this._isDataDirty) {
|
|
this.refresh();
|
|
}
|
|
this.ios.delegate = this._delegate;
|
|
}
|
|
|
|
public onUnloaded() {
|
|
this.ios.delegate = null;
|
|
super.onUnloaded();
|
|
}
|
|
|
|
get ios(): UITableView {
|
|
return this.nativeViewProtected;
|
|
}
|
|
|
|
get _childrenCount(): number {
|
|
return this._map.size;
|
|
}
|
|
|
|
public eachChildView(callback: (child: View) => boolean): void {
|
|
this._map.forEach((view, key) => {
|
|
callback(view);
|
|
});
|
|
}
|
|
|
|
public scrollToIndex(index: number) {
|
|
this._scrollToIndex(index, false);
|
|
}
|
|
|
|
public scrollToIndexAnimated(index: number) {
|
|
this._scrollToIndex(index);
|
|
}
|
|
|
|
private _scrollToIndex(index: number, animated: boolean = true) {
|
|
if (!this.ios) {
|
|
return;
|
|
}
|
|
|
|
const itemsLength = this.items ? this.items.length : 0;
|
|
// mimic Android behavior that silently coerces index values within [0, itemsLength - 1] range
|
|
if (itemsLength > 0) {
|
|
if (index < 0) {
|
|
index = 0;
|
|
} else if (index >= itemsLength) {
|
|
index = itemsLength - 1;
|
|
}
|
|
|
|
this.ios.scrollToRowAtIndexPathAtScrollPositionAnimated(NSIndexPath.indexPathForItemInSection(index, 0),
|
|
UITableViewScrollPosition.Top, animated);
|
|
} else if (trace.isEnabled()) {
|
|
trace.write(`Cannot scroll listview to index ${index} when listview items not set`, trace.categories.Binding);
|
|
}
|
|
}
|
|
|
|
public refresh() {
|
|
// clear bindingContext when it is not observable because otherwise bindings to items won't reevaluate
|
|
this._map.forEach((view, nativeView, map) => {
|
|
if (!(view.bindingContext instanceof Observable)) {
|
|
view.bindingContext = null;
|
|
}
|
|
});
|
|
|
|
if (this.isLoaded) {
|
|
this.ios.reloadData();
|
|
this.requestLayout();
|
|
this._isDataDirty = false;
|
|
} else {
|
|
this._isDataDirty = true;
|
|
}
|
|
}
|
|
|
|
public isItemAtIndexVisible(itemIndex: number): boolean {
|
|
const indexes: NSIndexPath[] = Array.from(this.ios.indexPathsForVisibleRows);
|
|
|
|
return indexes.some(visIndex => visIndex.row === itemIndex);
|
|
}
|
|
|
|
public getHeight(index: number): number {
|
|
return this._heights[index];
|
|
}
|
|
|
|
public setHeight(index: number, value: number): void {
|
|
this._heights[index] = value;
|
|
}
|
|
|
|
public _onRowHeightPropertyChanged(oldValue: Length, newValue: Length) {
|
|
const value = layout.toDeviceIndependentPixels(this._effectiveRowHeight);
|
|
const nativeView = this.ios;
|
|
if (value < 0) {
|
|
nativeView.rowHeight = UITableViewAutomaticDimension;
|
|
nativeView.estimatedRowHeight = DEFAULT_HEIGHT;
|
|
this._delegate = UITableViewDelegateImpl.initWithOwner(new WeakRef(this));
|
|
}
|
|
else {
|
|
nativeView.rowHeight = value;
|
|
nativeView.estimatedRowHeight = value;
|
|
this._delegate = UITableViewRowHeightDelegateImpl.initWithOwner(new WeakRef(this));
|
|
}
|
|
|
|
if (this.isLoaded) {
|
|
nativeView.delegate = this._delegate;
|
|
}
|
|
|
|
super._onRowHeightPropertyChanged(oldValue, newValue);
|
|
}
|
|
|
|
public requestLayout(): void {
|
|
// When preparing cell don't call super - no need to invalidate our measure when cell desiredSize is changed.
|
|
if (!this._preparingCell) {
|
|
super.requestLayout();
|
|
}
|
|
}
|
|
|
|
public measure(widthMeasureSpec: number, heightMeasureSpec: number): void {
|
|
this.widthMeasureSpec = widthMeasureSpec;
|
|
const changed = this._setCurrentMeasureSpecs(widthMeasureSpec, heightMeasureSpec);
|
|
super.measure(widthMeasureSpec, heightMeasureSpec);
|
|
if (changed) {
|
|
this.ios.reloadData();
|
|
}
|
|
}
|
|
|
|
public onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void {
|
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
|
|
this._map.forEach((childView, listViewCell) => {
|
|
View.measureChild(this, childView, childView._currentWidthMeasureSpec, childView._currentHeightMeasureSpec);
|
|
});
|
|
}
|
|
|
|
public onLayout(left: number, top: number, right: number, bottom: number): void {
|
|
super.onLayout(left, top, right, bottom);
|
|
|
|
this._map.forEach((childView, listViewCell) => {
|
|
let rowHeight = this._effectiveRowHeight;
|
|
let cellHeight = rowHeight > 0 ? rowHeight : this.getHeight(childView._listViewItemIndex);
|
|
if (cellHeight) {
|
|
let width = layout.getMeasureSpecSize(this.widthMeasureSpec);
|
|
childView.iosOverflowSafeAreaEnabled = false;
|
|
View.layoutChild(this, childView, 0, 0, width, cellHeight);
|
|
}
|
|
});
|
|
}
|
|
|
|
private _layoutCell(cellView: View, indexPath: NSIndexPath): number {
|
|
if (cellView) {
|
|
const rowHeight = this._effectiveRowHeight;
|
|
const heightMeasureSpec: number = rowHeight >= 0 ? layout.makeMeasureSpec(rowHeight, layout.EXACTLY) : infinity;
|
|
const measuredSize = View.measureChild(this, cellView, this.widthMeasureSpec, heightMeasureSpec);
|
|
const height = measuredSize.measuredHeight;
|
|
this.setHeight(indexPath.row, height);
|
|
|
|
return height;
|
|
}
|
|
|
|
return this.ios.estimatedRowHeight;
|
|
}
|
|
|
|
public _prepareCell(cell: ListViewCell, indexPath: NSIndexPath): number {
|
|
let cellHeight: number;
|
|
try {
|
|
this._preparingCell = true;
|
|
let view: ItemView = cell.view;
|
|
if (!view) {
|
|
view = this._getItemTemplate(indexPath.row).createView();
|
|
}
|
|
|
|
let args = notifyForItemAtIndex(this, cell, view, ITEMLOADING, indexPath);
|
|
view = args.view || this._getDefaultItemContent(indexPath.row);
|
|
|
|
// Proxy containers should not get treated as layouts.
|
|
// Wrap them in a real layout as well.
|
|
if (view instanceof ProxyViewContainer) {
|
|
let sp = new StackLayout();
|
|
sp.addChild(view);
|
|
view = sp;
|
|
}
|
|
|
|
// If cell is reused it have old content - remove it first.
|
|
if (!cell.view) {
|
|
cell.owner = new WeakRef(view);
|
|
} else if (cell.view !== view) {
|
|
this._removeContainer(cell);
|
|
(<UIView>cell.view.nativeViewProtected).removeFromSuperview();
|
|
cell.owner = new WeakRef(view);
|
|
}
|
|
|
|
this._prepareItem(view, indexPath.row);
|
|
view._listViewItemIndex = indexPath.row;
|
|
this._map.set(cell, view);
|
|
|
|
// We expect that views returned from itemLoading are new (e.g. not reused).
|
|
if (view && !view.parent) {
|
|
this._addView(view);
|
|
cell.contentView.addSubview(view.nativeViewProtected);
|
|
}
|
|
|
|
cellHeight = this._layoutCell(view, indexPath);
|
|
} finally {
|
|
this._preparingCell = false;
|
|
}
|
|
|
|
return cellHeight;
|
|
}
|
|
|
|
public _removeContainer(cell: ListViewCell): void {
|
|
let view: ItemView = cell.view;
|
|
// This is to clear the StackLayout that is used to wrap ProxyViewContainer instances.
|
|
if (!(view.parent instanceof ListView)) {
|
|
this._removeView(view.parent);
|
|
}
|
|
|
|
// No need to request layout when we are removing cells.
|
|
const preparing = this._preparingCell;
|
|
this._preparingCell = true;
|
|
view.parent._removeView(view);
|
|
view._listViewItemIndex = undefined;
|
|
this._preparingCell = preparing;
|
|
this._map.delete(cell);
|
|
}
|
|
|
|
[separatorColorProperty.getDefault](): UIColor {
|
|
return this.ios.separatorColor;
|
|
}
|
|
[separatorColorProperty.setNative](value: Color | UIColor) {
|
|
this.ios.separatorColor = value instanceof Color ? value.ios : value;
|
|
}
|
|
|
|
[itemTemplatesProperty.getDefault](): KeyedTemplate[] {
|
|
return null;
|
|
}
|
|
[itemTemplatesProperty.setNative](value: KeyedTemplate[]) {
|
|
this._itemTemplatesInternal = new Array<KeyedTemplate>(this._defaultTemplate);
|
|
if (value) {
|
|
for (let i = 0, length = value.length; i < length; i++) {
|
|
this.ios.registerClassForCellReuseIdentifier(ListViewCell.class(), value[i].key);
|
|
}
|
|
this._itemTemplatesInternal = this._itemTemplatesInternal.concat(value);
|
|
}
|
|
|
|
this.refresh();
|
|
}
|
|
|
|
[iosEstimatedRowHeightProperty.getDefault](): Length {
|
|
return DEFAULT_HEIGHT;
|
|
}
|
|
[iosEstimatedRowHeightProperty.setNative](value: Length) {
|
|
const nativeView = this.ios;
|
|
const estimatedHeight = Length.toDevicePixels(value, 0);
|
|
nativeView.estimatedRowHeight = estimatedHeight < 0 ? DEFAULT_HEIGHT : estimatedHeight;
|
|
}
|
|
}
|