mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
feat(ios): SplitView
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import { Application } from '@nativescript/core';
|
||||
import { Application, SplitView } from '@nativescript/core';
|
||||
|
||||
Application.run({ moduleName: 'app-root' });
|
||||
// Application.run({ moduleName: 'app-root' });
|
||||
|
||||
SplitView.SplitStyle = 'triple';
|
||||
Application.run({ moduleName: 'split-view/split-view-root' });
|
||||
|
||||
23
apps/toolbox/src/split-view/split-view-primary.ts
Normal file
23
apps/toolbox/src/split-view/split-view-primary.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Observable, EventData, Page, SplitView, ItemEventData } from '@nativescript/core';
|
||||
import { getItemCallbacks } from './split-view-root';
|
||||
let page: Page;
|
||||
|
||||
export function navigatingTo(args: EventData) {
|
||||
page = <Page>args.object;
|
||||
page.bindingContext = new SplitViewPrimaryModel();
|
||||
}
|
||||
|
||||
export class SplitViewPrimaryModel extends Observable {
|
||||
items: string[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.items = Array.from({ length: 20 }, (_, i) => `Item ${i + 1}`);
|
||||
}
|
||||
|
||||
onItemTap(args: ItemEventData) {
|
||||
console.log('args.index', args.index);
|
||||
SplitView.getInstance()?.showSecondary();
|
||||
getItemCallbacks().forEach((callback) => callback(this.items[args.index]));
|
||||
}
|
||||
}
|
||||
16
apps/toolbox/src/split-view/split-view-primary.xml
Normal file
16
apps/toolbox/src/split-view/split-view-primary.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
|
||||
<ActionBar title="Primary View" class="action-bar" iosLargeTitle="true"></ActionBar>
|
||||
|
||||
<!-- Primary column (master) -->
|
||||
<GridLayout rows="auto,*">
|
||||
<!-- <Label text="Primary" marginBottom="12" marginLeft="8" fontSize="22" fontWeight="bold" /> -->
|
||||
<ListView row="1" items="{{ items }}" itemTap="{{ onItemTap }}" backgroundColor="white">
|
||||
<ListView.itemTemplate>
|
||||
<GridLayout padding="12">
|
||||
<Label text="{{ $value }}" fontSize="18" />
|
||||
</GridLayout>
|
||||
</ListView.itemTemplate>
|
||||
</ListView>
|
||||
</GridLayout>
|
||||
|
||||
</Page>
|
||||
17
apps/toolbox/src/split-view/split-view-root.ts
Normal file
17
apps/toolbox/src/split-view/split-view-root.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Observable, EventData, Page } from '@nativescript/core';
|
||||
let page: Page;
|
||||
|
||||
export function navigatingTo(args: EventData) {
|
||||
page = <Page>args.object;
|
||||
page.bindingContext = new SplitViewModel();
|
||||
}
|
||||
|
||||
export class SplitViewModel extends Observable {}
|
||||
|
||||
let itemCallbacks: Array<(item: any) => void> = [];
|
||||
export function setItemCallbacks(changeItem: Array<(item: any) => void>) {
|
||||
itemCallbacks.push(...changeItem);
|
||||
}
|
||||
export function getItemCallbacks() {
|
||||
return itemCallbacks;
|
||||
}
|
||||
14
apps/toolbox/src/split-view/split-view-root.xml
Normal file
14
apps/toolbox/src/split-view/split-view-root.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<SplitView xmlns="http://schemas.nativescript.org/tns.xsd" displayMode="twoBesideSecondary" splitBehavior="tile" preferredPrimaryColumnWidthFraction="0.25" preferredSupplementaryColumnWidthFraction="0.33">
|
||||
<Frame splitRole="primary" defaultPage="split-view/split-view-primary">
|
||||
|
||||
</Frame>
|
||||
|
||||
<Frame splitRole="secondary" defaultPage="split-view/split-view-secondary">
|
||||
|
||||
</Frame>
|
||||
|
||||
<Frame splitRole="supplementary" defaultPage="split-view/split-view-supplement">
|
||||
|
||||
</Frame>
|
||||
|
||||
</SplitView>
|
||||
24
apps/toolbox/src/split-view/split-view-secondary.ts
Normal file
24
apps/toolbox/src/split-view/split-view-secondary.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Observable, EventData, Page, SplitView } from '@nativescript/core';
|
||||
import { setItemCallbacks } from './split-view-root';
|
||||
let page: Page;
|
||||
|
||||
export function navigatingTo(args: EventData) {
|
||||
page = <Page>args.object;
|
||||
page.bindingContext = new SplitViewSecondaryModel();
|
||||
}
|
||||
|
||||
export class SplitViewSecondaryModel extends Observable {
|
||||
selectedItem = `Select an item from Primary.`;
|
||||
constructor() {
|
||||
super();
|
||||
setItemCallbacks([this.changeItem.bind(this)]);
|
||||
}
|
||||
toggle() {
|
||||
SplitView.getInstance()?.showPrimary();
|
||||
}
|
||||
|
||||
changeItem(item: any) {
|
||||
this.selectedItem = item;
|
||||
this.notifyPropertyChange('selectedItem', item);
|
||||
}
|
||||
}
|
||||
10
apps/toolbox/src/split-view/split-view-secondary.xml
Normal file
10
apps/toolbox/src/split-view/split-view-secondary.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
|
||||
<ActionBar title="Secondary View" class="action-bar">
|
||||
</ActionBar>
|
||||
<!-- Secondary column (detail) -->
|
||||
<StackLayout class="p-16">
|
||||
<Label text="Secondary" marginBottom="12" fontSize="22" fontWeight="bold" />
|
||||
<Label text="{{ selectedItem }}" textWrap="true" tap="{{toggle}}" />
|
||||
</StackLayout>
|
||||
|
||||
</Page>
|
||||
24
apps/toolbox/src/split-view/split-view-supplement.ts
Normal file
24
apps/toolbox/src/split-view/split-view-supplement.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Observable, EventData, Page, SplitView } from '@nativescript/core';
|
||||
import { setItemCallbacks } from './split-view-root';
|
||||
let page: Page;
|
||||
|
||||
export function navigatingTo(args: EventData) {
|
||||
page = <Page>args.object;
|
||||
page.bindingContext = new SplitViewSupplementaryModel();
|
||||
}
|
||||
|
||||
export class SplitViewSupplementaryModel extends Observable {
|
||||
selectedItem = `Supplementary - Select an item.`;
|
||||
constructor() {
|
||||
super();
|
||||
setItemCallbacks([this.changeItem.bind(this)]);
|
||||
}
|
||||
toggle() {
|
||||
SplitView.getInstance()?.showPrimary();
|
||||
}
|
||||
|
||||
changeItem(item: any) {
|
||||
this.selectedItem = item;
|
||||
this.notifyPropertyChange('selectedItem', item);
|
||||
}
|
||||
}
|
||||
10
apps/toolbox/src/split-view/split-view-supplement.xml
Normal file
10
apps/toolbox/src/split-view/split-view-supplement.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
|
||||
<ActionBar title="Supplementary View" class="action-bar">
|
||||
</ActionBar>
|
||||
<!-- Supplementary column (detail) -->
|
||||
<StackLayout class="p-16">
|
||||
<Label text="Supplementary" marginBottom="12" fontSize="22" fontWeight="bold" />
|
||||
<Label text="{{ selectedItem }}" textWrap="true" tap="{{toggle}}" />
|
||||
</StackLayout>
|
||||
|
||||
</Page>
|
||||
@@ -71,6 +71,7 @@ export { SegmentedBar, SegmentedBarItem } from './segmented-bar';
|
||||
export type { SelectedIndexChangedEventData } from './segmented-bar';
|
||||
export { Slider } from './slider';
|
||||
export type { AccessibilityDecrementEventData, AccessibilityIncrementEventData } from './slider';
|
||||
export { SplitView } from './split-view';
|
||||
|
||||
export { addTaggedAdditionalCSS, removeTaggedAdditionalCSS, resolveFileNameFromUrl } from './styling/style-scope';
|
||||
export { Background } from './styling/background';
|
||||
|
||||
7
packages/core/ui/split-view/index.android.ts
Normal file
7
packages/core/ui/split-view/index.android.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { SplitViewBase } from './split-view-common';
|
||||
|
||||
export { SplitBehavior, SplitRole, SplitStyle, SplitDisplayMode } from './split-view-common';
|
||||
|
||||
export class SplitView extends SplitViewBase {
|
||||
// Android does not have a native SplitViewController equivalent.
|
||||
}
|
||||
11
packages/core/ui/split-view/index.d.ts
vendored
Normal file
11
packages/core/ui/split-view/index.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import { SplitViewBase } from './split-view-common';
|
||||
|
||||
export type { SplitBehavior, SplitRole, SplitStyle, SplitDisplayMode } from './split-view-common';
|
||||
|
||||
/**
|
||||
* iOS UISplitViewController-backed container.
|
||||
* On Android, acts as a simple container.
|
||||
*
|
||||
* @nsView SplitView
|
||||
*/
|
||||
export class SplitView extends SplitViewBase {}
|
||||
344
packages/core/ui/split-view/index.ios.ts
Normal file
344
packages/core/ui/split-view/index.ios.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
import { SplitViewBase, SplitRole, displayModeProperty, splitBehaviorProperty, preferredPrimaryColumnWidthFractionProperty, preferredSupplementaryColumnWidthFractionProperty } from './split-view-common';
|
||||
import { View } from '../core/view';
|
||||
import { layout } from '../../utils';
|
||||
|
||||
@NativeClass
|
||||
class UISplitViewControllerDelegateImpl extends NSObject implements UISplitViewControllerDelegate {
|
||||
public static ObjCProtocols = [UISplitViewControllerDelegate];
|
||||
private _owner: WeakRef<SplitView>;
|
||||
|
||||
public static initWithOwner(owner: WeakRef<SplitView>): UISplitViewControllerDelegateImpl {
|
||||
const d = <UISplitViewControllerDelegateImpl>UISplitViewControllerDelegateImpl.new();
|
||||
d._owner = owner;
|
||||
return d;
|
||||
}
|
||||
|
||||
splitViewControllerCollapseSecondaryViewControllerOntoPrimaryViewController(splitViewController: UISplitViewController, secondaryViewController: UIViewController, primaryViewController: UIViewController): boolean {
|
||||
const owner = this._owner.deref();
|
||||
if (owner) {
|
||||
// Notify the owner about the collapse action
|
||||
owner.onSecondaryViewCollapsed(secondaryViewController, primaryViewController);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
splitViewControllerDidCollapse(svc: UISplitViewController): void {
|
||||
// Can be used to notify owner if needed
|
||||
}
|
||||
|
||||
splitViewControllerDidExpand(svc: UISplitViewController): void {
|
||||
// Can be used to notify owner if needed
|
||||
}
|
||||
|
||||
splitViewControllerDidHideColumn(svc: UISplitViewController, column: UISplitViewControllerColumn): void {
|
||||
// Can be used to notify owner if needed
|
||||
}
|
||||
|
||||
splitViewControllerDidShowColumn(svc: UISplitViewController, column: UISplitViewControllerColumn): void {
|
||||
// Can be used to notify owner if needed
|
||||
}
|
||||
|
||||
splitViewControllerDisplayModeForExpandingToProposedDisplayMode(svc: UISplitViewController, proposedDisplayMode: UISplitViewControllerDisplayMode): UISplitViewControllerDisplayMode {
|
||||
return UISplitViewControllerDisplayMode.TwoBesideSecondary;
|
||||
}
|
||||
|
||||
splitViewControllerTopColumnForCollapsingToProposedTopColumn(svc: UISplitViewController, proposedTopColumn: UISplitViewControllerColumn): UISplitViewControllerColumn {
|
||||
return UISplitViewControllerColumn.Secondary;
|
||||
}
|
||||
|
||||
toggleSidebar(sender: UIBarButtonItem): void {
|
||||
const owner = this._owner.deref();
|
||||
if (owner) {
|
||||
owner.showPrimary();
|
||||
}
|
||||
}
|
||||
|
||||
static ObjCExposedMethods = {
|
||||
'toggleSidebar:': { returns: interop.types.void, params: [UIBarButtonItem] },
|
||||
};
|
||||
}
|
||||
|
||||
export class SplitView extends SplitViewBase {
|
||||
static instance: SplitView;
|
||||
static getInstance(): SplitViewBase | null {
|
||||
return SplitView.instance;
|
||||
}
|
||||
|
||||
public viewController: UISplitViewController;
|
||||
private _delegate: UISplitViewControllerDelegateImpl;
|
||||
// Keep role -> controller
|
||||
private _controllers = new Map<SplitRole, UIViewController | UINavigationController>();
|
||||
private _children = new Map<SplitRole, View>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// Prefer modern initializer when available; otherwise default
|
||||
console.log('this._getSplitStyle(): ', this._getSplitStyle());
|
||||
this.viewController = UISplitViewController.alloc().initWithStyle(this._getSplitStyle());
|
||||
}
|
||||
|
||||
createNativeView() {
|
||||
SplitView.instance = this;
|
||||
this._delegate = UISplitViewControllerDelegateImpl.initWithOwner(new WeakRef(this));
|
||||
this.viewController.delegate = this._delegate;
|
||||
this.viewController.presentsWithGesture = true;
|
||||
|
||||
// Apply initial preferences
|
||||
this._applyPreferences();
|
||||
|
||||
return this.viewController.view;
|
||||
}
|
||||
|
||||
disposeNativeView(): void {
|
||||
super.disposeNativeView();
|
||||
this._controllers.clear();
|
||||
this._children.clear();
|
||||
this.viewController = null;
|
||||
this._delegate = null;
|
||||
}
|
||||
|
||||
private _getSplitStyle() {
|
||||
switch (SplitView.SplitStyle) {
|
||||
case 'triple':
|
||||
return UISplitViewControllerStyle.TripleColumn;
|
||||
default:
|
||||
// default to double always
|
||||
return UISplitViewControllerStyle.DoubleColumn;
|
||||
}
|
||||
}
|
||||
|
||||
// Controller-backed container: intercept native tree operations
|
||||
public _addViewToNativeVisualTree(child: SplitViewBase, atIndex: number): boolean {
|
||||
const role = this._resolveRoleForChild(child, atIndex);
|
||||
const controller = this._ensureControllerForChild(child);
|
||||
console.log('set controllers for role: ' + role);
|
||||
console.log('controller: ', controller);
|
||||
this._children.set(role, child);
|
||||
this._controllers.set(role, controller);
|
||||
this._syncControllers();
|
||||
return true;
|
||||
}
|
||||
|
||||
public _removeViewFromNativeVisualTree(child: View): void {
|
||||
const role = this._findRoleByChild(child);
|
||||
if (role) {
|
||||
this._children.delete(role);
|
||||
this._controllers.delete(role);
|
||||
this._syncControllers();
|
||||
}
|
||||
super._removeViewFromNativeVisualTree(child);
|
||||
}
|
||||
|
||||
public onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
const width = layout.getMeasureSpecSize(widthMeasureSpec);
|
||||
const widthMode = layout.getMeasureSpecMode(widthMeasureSpec);
|
||||
const height = layout.getMeasureSpecSize(heightMeasureSpec);
|
||||
const heightMode = layout.getMeasureSpecMode(heightMeasureSpec);
|
||||
|
||||
const horizontal = this.effectivePaddingLeft + this.effectivePaddingRight + this.effectiveBorderLeftWidth + this.effectiveBorderRightWidth;
|
||||
const vertical = this.effectivePaddingTop + this.effectivePaddingBottom + this.effectiveBorderTopWidth + this.effectiveBorderBottomWidth;
|
||||
|
||||
const measuredWidth = Math.max(widthMode === layout.UNSPECIFIED ? 0 : width, this.effectiveMinWidth) + (widthMode === layout.UNSPECIFIED ? horizontal : 0);
|
||||
const measuredHeight = Math.max(heightMode === layout.UNSPECIFIED ? 0 : height, this.effectiveMinHeight) + (heightMode === layout.UNSPECIFIED ? vertical : 0);
|
||||
|
||||
const widthAndState = View.resolveSizeAndState(measuredWidth, width, widthMode, 0);
|
||||
const heightAndState = View.resolveSizeAndState(measuredHeight, height, heightMode, 0);
|
||||
this.setMeasuredDimension(widthAndState, heightAndState);
|
||||
}
|
||||
|
||||
public onRoleChanged(view: View, oldValue: SplitRole, newValue: SplitRole) {
|
||||
// Move child mapping to new role and resync
|
||||
const oldRole = this._findRoleByChild(view);
|
||||
if (oldRole) {
|
||||
const controller = this._controllers.get(oldRole);
|
||||
this._controllers.delete(oldRole);
|
||||
this._children.delete(oldRole);
|
||||
if (controller) {
|
||||
this._controllers.set(newValue, controller);
|
||||
}
|
||||
this._children.set(newValue, view);
|
||||
this._syncControllers();
|
||||
}
|
||||
}
|
||||
|
||||
onSecondaryViewCollapsed(secondaryViewController: UIViewController, primaryViewController: UIViewController): void {
|
||||
// Default implementation: do nothing.
|
||||
// Subclasses may override to customize behavior when secondary is collapsed onto primary.
|
||||
}
|
||||
|
||||
showPrimary(): void {
|
||||
if (!this.viewController) return;
|
||||
this.viewController.showColumn(UISplitViewControllerColumn.Primary);
|
||||
}
|
||||
|
||||
hidePrimary(): void {
|
||||
if (!this.viewController) return;
|
||||
this.viewController.hideColumn(UISplitViewControllerColumn.Primary);
|
||||
}
|
||||
|
||||
showSecondary(): void {
|
||||
if (!this.viewController) return;
|
||||
this.viewController.showColumn(UISplitViewControllerColumn.Secondary);
|
||||
}
|
||||
|
||||
hideSecondary(): void {
|
||||
if (!this.viewController) return;
|
||||
this.viewController.hideColumn(UISplitViewControllerColumn.Secondary);
|
||||
}
|
||||
|
||||
showSupplementary(): void {
|
||||
if (!this.viewController) return;
|
||||
this.viewController.showColumn(UISplitViewControllerColumn.Supplementary);
|
||||
}
|
||||
|
||||
private _resolveRoleForChild(child: SplitViewBase, atIndex: number): SplitRole {
|
||||
const explicit = SplitViewBase.getRole(child);
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
// Fallback by index if no explicit role set
|
||||
return this._roleByIndex(atIndex) as SplitRole;
|
||||
}
|
||||
|
||||
private _findRoleByChild(child: View): SplitRole | null {
|
||||
for (const [role, c] of this._children.entries()) {
|
||||
if (c === child) {
|
||||
return role;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _ensureControllerForChild(child: View): UIViewController {
|
||||
// If child is controller-backed (Page/Frame/etc.), reuse its controller
|
||||
const vc = (child.ios instanceof UIViewController ? (child.ios as any) : (child as any).viewController) as UIViewController | null;
|
||||
if (vc) {
|
||||
return vc;
|
||||
}
|
||||
// Fallback: basic wrapper (not expected in current usage where children are Frames/Pages)
|
||||
const wrapper = UIViewController.new();
|
||||
if (!wrapper.view) {
|
||||
wrapper.view = UIView.new();
|
||||
}
|
||||
if (child.nativeViewProtected) {
|
||||
wrapper.view.addSubview(child.nativeViewProtected);
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private _syncControllers(): void {
|
||||
if (!this.viewController) {
|
||||
return;
|
||||
}
|
||||
// Prefer modern API if present; otherwise fall back to setting viewControllers array
|
||||
const primary = this._controllers.get('primary');
|
||||
const secondary = this._controllers.get('secondary');
|
||||
const supplementary = this._controllers.get('supplementary');
|
||||
|
||||
if (primary) {
|
||||
this.viewController.setViewControllerForColumn(primary, UISplitViewControllerColumn.Primary);
|
||||
}
|
||||
if (secondary) {
|
||||
this.viewController.setViewControllerForColumn(secondary, UISplitViewControllerColumn.Secondary);
|
||||
}
|
||||
if (supplementary) {
|
||||
this.viewController.setViewControllerForColumn(supplementary, UISplitViewControllerColumn.Supplementary);
|
||||
}
|
||||
|
||||
this._applyPreferences();
|
||||
}
|
||||
|
||||
private _applyPreferences(): void {
|
||||
if (!this.viewController) {
|
||||
return;
|
||||
}
|
||||
|
||||
// displayMode
|
||||
let preferredDisplayMode = UISplitViewControllerDisplayMode.Automatic;
|
||||
switch (this.displayMode) {
|
||||
case 'secondaryOnly':
|
||||
preferredDisplayMode = UISplitViewControllerDisplayMode.SecondaryOnly;
|
||||
break;
|
||||
case 'oneBesideSecondary':
|
||||
preferredDisplayMode = UISplitViewControllerDisplayMode.OneBesideSecondary;
|
||||
break;
|
||||
case 'oneOverSecondary':
|
||||
preferredDisplayMode = UISplitViewControllerDisplayMode.OneOverSecondary;
|
||||
break;
|
||||
case 'twoBesideSecondary':
|
||||
preferredDisplayMode = UISplitViewControllerDisplayMode.TwoBesideSecondary;
|
||||
break;
|
||||
case 'twoOverSecondary':
|
||||
preferredDisplayMode = UISplitViewControllerDisplayMode.TwoOverSecondary;
|
||||
break;
|
||||
case 'twoDisplaceSecondary':
|
||||
preferredDisplayMode = UISplitViewControllerDisplayMode.TwoDisplaceSecondary;
|
||||
break;
|
||||
}
|
||||
this.viewController.preferredDisplayMode = preferredDisplayMode;
|
||||
|
||||
// splitBehavior (iOS 14+)
|
||||
const sb = this.splitBehavior;
|
||||
let preferredSplitBehavior = UISplitViewControllerSplitBehavior.Automatic;
|
||||
switch (sb) {
|
||||
case 'tile':
|
||||
preferredSplitBehavior = UISplitViewControllerSplitBehavior.Tile;
|
||||
break;
|
||||
case 'overlay':
|
||||
preferredSplitBehavior = UISplitViewControllerSplitBehavior.Overlay ?? UISplitViewControllerSplitBehavior.Automatic;
|
||||
break;
|
||||
case 'displace':
|
||||
preferredSplitBehavior = UISplitViewControllerSplitBehavior.Displace ?? UISplitViewControllerSplitBehavior.Automatic;
|
||||
break;
|
||||
}
|
||||
this.viewController.preferredSplitBehavior = preferredSplitBehavior;
|
||||
|
||||
const primary = this._controllers.get('primary');
|
||||
const secondary = this._controllers.get('secondary');
|
||||
const supplementary = this._controllers.get('supplementary');
|
||||
if (secondary instanceof UINavigationController && secondary.navigationItem) {
|
||||
// TODO: add properties to customize this
|
||||
secondary.navigationItem.leftBarButtonItem = this.viewController.displayModeButtonItem;
|
||||
|
||||
// Optional: slightly larger symbol weight/size
|
||||
// const cfg = UIImageSymbolConfiguration.configurationWithPointSizeWeightScale(18, UIImageSymbolWeight.Regular, UIImageSymbolScale.Medium);
|
||||
// const image = UIImage.systemImageNamedWithConfiguration('sidebar.left', cfg);
|
||||
|
||||
// const item = UIBarButtonItem.alloc().initWithImageStyleTargetAction(image, UIBarButtonItemStyle.Plain, this._delegate, 'toggleSidebar:');
|
||||
// secondary.navigationItem.leftBarButtonItem = item;
|
||||
secondary.navigationItem.leftItemsSupplementBackButton = true;
|
||||
}
|
||||
if (supplementary) {
|
||||
this.showSupplementary();
|
||||
}
|
||||
|
||||
// Width fractions
|
||||
if (typeof this.preferredPrimaryColumnWidthFraction === 'number' && !isNaN(this.preferredPrimaryColumnWidthFraction)) {
|
||||
this.viewController.preferredPrimaryColumnWidthFraction = this.preferredPrimaryColumnWidthFraction;
|
||||
}
|
||||
if (SplitView.SplitStyle === 'triple') {
|
||||
// supplementary only applies in triple style
|
||||
if (typeof this.preferredSupplementaryColumnWidthFraction === 'number' && !isNaN(this.preferredSupplementaryColumnWidthFraction)) {
|
||||
this.viewController.preferredSupplementaryColumnWidthFraction = this.preferredSupplementaryColumnWidthFraction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[displayModeProperty.setNative](value: string) {
|
||||
this._applyPreferences();
|
||||
}
|
||||
|
||||
[splitBehaviorProperty.setNative](value: string) {
|
||||
this._applyPreferences();
|
||||
}
|
||||
|
||||
[preferredPrimaryColumnWidthFractionProperty.setNative](value: number) {
|
||||
this._applyPreferences();
|
||||
}
|
||||
|
||||
[preferredSupplementaryColumnWidthFractionProperty.setNative](value: number) {
|
||||
this._applyPreferences();
|
||||
}
|
||||
}
|
||||
133
packages/core/ui/split-view/split-view-common.ts
Normal file
133
packages/core/ui/split-view/split-view-common.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { LayoutBase } from '../layouts/layout-base';
|
||||
import { View, CSSType } from '../core/view';
|
||||
import { Property, makeParser, makeValidator } from '../core/properties';
|
||||
|
||||
export type SplitRole = 'primary' | 'secondary' | 'supplementary';
|
||||
const splitRoleConverter = makeParser<SplitRole>(makeValidator<SplitRole>('primary', 'secondary', 'supplementary'));
|
||||
|
||||
export type SplitStyle = 'automatic' | 'double' | 'triple';
|
||||
|
||||
export type SplitDisplayMode = 'automatic' | 'secondaryOnly' | 'oneBesideSecondary' | 'oneOverSecondary' | 'twoBesideSecondary' | 'twoOverSecondary' | 'twoDisplaceSecondary';
|
||||
const splitDisplayModeConverter = makeParser<SplitDisplayMode>(makeValidator<SplitDisplayMode>('automatic', 'secondaryOnly', 'oneBesideSecondary', 'oneOverSecondary', 'twoBesideSecondary', 'twoOverSecondary', 'twoDisplaceSecondary'));
|
||||
|
||||
export type SplitBehavior = 'automatic' | 'tile' | 'overlay' | 'displace';
|
||||
const splitBehaviorConverter = makeParser<SplitBehavior>(makeValidator<SplitBehavior>('automatic', 'tile', 'overlay', 'displace'));
|
||||
|
||||
// Default child roles (helps authoring without setting splitRole on children)
|
||||
const ROLE_ORDER: SplitRole[] = ['primary', 'secondary', 'supplementary'];
|
||||
|
||||
@CSSType('SplitView')
|
||||
export class SplitViewBase extends LayoutBase {
|
||||
/**
|
||||
* The master display split style display settings.
|
||||
* Must be set before bootstrapping the app.
|
||||
*/
|
||||
static SplitStyle: SplitStyle;
|
||||
|
||||
static getInstance(): SplitViewBase | null {
|
||||
// Platform-specific implementations may override
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Child role (primary, secondary, supplementary) */
|
||||
splitRole: SplitRole;
|
||||
/** Preferred display mode */
|
||||
displayMode: SplitDisplayMode;
|
||||
/** Preferred split behavior (iOS 14+) */
|
||||
splitBehavior: SplitBehavior;
|
||||
/** Primary column width fraction (0..1) */
|
||||
preferredPrimaryColumnWidthFraction: number;
|
||||
/** Supplementary column width fraction (0..1, iOS 14+ triple) */
|
||||
preferredSupplementaryColumnWidthFraction: number;
|
||||
|
||||
/**
|
||||
* Get child role (primary, secondary, supplementary)
|
||||
*/
|
||||
public static getRole(element: SplitViewBase): SplitRole {
|
||||
return element.splitRole;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set child role (primary, secondary, supplementary)
|
||||
*/
|
||||
public static setRole(element: SplitViewBase, value: SplitRole): void {
|
||||
element.splitRole = value;
|
||||
}
|
||||
|
||||
// Called when a child's role changes; platform impls may override
|
||||
public onRoleChanged(view: View, oldValue: SplitRole, newValue: SplitRole) {
|
||||
this.requestLayout();
|
||||
}
|
||||
|
||||
showPrimary() {
|
||||
// Platform-specific implementations may override
|
||||
}
|
||||
|
||||
hidePrimary() {
|
||||
// Platform-specific implementations may override
|
||||
}
|
||||
|
||||
showSecondary() {
|
||||
// Platform-specific implementations may override
|
||||
}
|
||||
|
||||
hideSecondary() {
|
||||
// Platform-specific implementations may override
|
||||
}
|
||||
|
||||
showSupplementary() {
|
||||
// Platform-specific implementations may override
|
||||
}
|
||||
|
||||
// Utility to infer a role by index when none specified
|
||||
protected _roleByIndex(index: number): SplitRole {
|
||||
return ROLE_ORDER[Math.max(0, Math.min(index, ROLE_ORDER.length - 1))];
|
||||
}
|
||||
}
|
||||
|
||||
SplitViewBase.prototype.recycleNativeView = 'auto';
|
||||
|
||||
export const splitRoleProperty = new Property<View, SplitRole>({
|
||||
name: 'splitRole',
|
||||
defaultValue: 'primary',
|
||||
valueChanged: (target, oldValue, newValue) => {
|
||||
const parent = target.parent;
|
||||
if (parent instanceof SplitViewBase) {
|
||||
parent.onRoleChanged(target, oldValue, newValue);
|
||||
}
|
||||
},
|
||||
valueConverter: splitRoleConverter,
|
||||
});
|
||||
splitRoleProperty.register(View);
|
||||
|
||||
export const displayModeProperty = new Property<SplitViewBase, SplitDisplayMode>({
|
||||
name: 'displayMode',
|
||||
defaultValue: 'automatic',
|
||||
affectsLayout: __APPLE__,
|
||||
valueConverter: splitDisplayModeConverter,
|
||||
});
|
||||
displayModeProperty.register(SplitViewBase);
|
||||
|
||||
export const splitBehaviorProperty = new Property<SplitViewBase, SplitBehavior>({
|
||||
name: 'splitBehavior',
|
||||
defaultValue: 'automatic',
|
||||
affectsLayout: __APPLE__,
|
||||
valueConverter: splitBehaviorConverter,
|
||||
});
|
||||
splitBehaviorProperty.register(SplitViewBase);
|
||||
|
||||
export const preferredPrimaryColumnWidthFractionProperty = new Property<SplitViewBase, number>({
|
||||
name: 'preferredPrimaryColumnWidthFraction',
|
||||
defaultValue: 0,
|
||||
affectsLayout: __APPLE__,
|
||||
valueConverter: (v) => Math.max(0, Math.min(1, parseFloat(v))),
|
||||
});
|
||||
preferredPrimaryColumnWidthFractionProperty.register(SplitViewBase);
|
||||
|
||||
export const preferredSupplementaryColumnWidthFractionProperty = new Property<SplitViewBase, number>({
|
||||
name: 'preferredSupplementaryColumnWidthFraction',
|
||||
defaultValue: 0,
|
||||
affectsLayout: __APPLE__,
|
||||
valueConverter: (v) => Math.max(0, Math.min(1, parseFloat(v))),
|
||||
});
|
||||
preferredSupplementaryColumnWidthFractionProperty.register(SplitViewBase);
|
||||
Reference in New Issue
Block a user