From c5cef2143119ff88ef23024b317d6211c9b43e1a Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Wed, 22 Oct 2025 20:56:45 -0700 Subject: [PATCH] feat(ios): SplitView --- apps/toolbox/src/main.ts | 7 +- .../src/split-view/split-view-primary.ts | 23 ++ .../src/split-view/split-view-primary.xml | 16 + .../toolbox/src/split-view/split-view-root.ts | 17 + .../src/split-view/split-view-root.xml | 14 + .../src/split-view/split-view-secondary.ts | 24 ++ .../src/split-view/split-view-secondary.xml | 10 + .../src/split-view/split-view-supplement.ts | 24 ++ .../src/split-view/split-view-supplement.xml | 10 + packages/core/ui/index.ts | 1 + packages/core/ui/split-view/index.android.ts | 7 + packages/core/ui/split-view/index.d.ts | 11 + packages/core/ui/split-view/index.ios.ts | 344 ++++++++++++++++++ .../core/ui/split-view/split-view-common.ts | 133 +++++++ 14 files changed, 639 insertions(+), 2 deletions(-) create mode 100644 apps/toolbox/src/split-view/split-view-primary.ts create mode 100644 apps/toolbox/src/split-view/split-view-primary.xml create mode 100644 apps/toolbox/src/split-view/split-view-root.ts create mode 100644 apps/toolbox/src/split-view/split-view-root.xml create mode 100644 apps/toolbox/src/split-view/split-view-secondary.ts create mode 100644 apps/toolbox/src/split-view/split-view-secondary.xml create mode 100644 apps/toolbox/src/split-view/split-view-supplement.ts create mode 100644 apps/toolbox/src/split-view/split-view-supplement.xml create mode 100644 packages/core/ui/split-view/index.android.ts create mode 100644 packages/core/ui/split-view/index.d.ts create mode 100644 packages/core/ui/split-view/index.ios.ts create mode 100644 packages/core/ui/split-view/split-view-common.ts diff --git a/apps/toolbox/src/main.ts b/apps/toolbox/src/main.ts index a4c5c529a..bdf321481 100644 --- a/apps/toolbox/src/main.ts +++ b/apps/toolbox/src/main.ts @@ -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' }); diff --git a/apps/toolbox/src/split-view/split-view-primary.ts b/apps/toolbox/src/split-view/split-view-primary.ts new file mode 100644 index 000000000..3f5748cb2 --- /dev/null +++ b/apps/toolbox/src/split-view/split-view-primary.ts @@ -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 = 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])); + } +} diff --git a/apps/toolbox/src/split-view/split-view-primary.xml b/apps/toolbox/src/split-view/split-view-primary.xml new file mode 100644 index 000000000..b5d870121 --- /dev/null +++ b/apps/toolbox/src/split-view/split-view-primary.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/toolbox/src/split-view/split-view-root.ts b/apps/toolbox/src/split-view/split-view-root.ts new file mode 100644 index 000000000..9cb11fce7 --- /dev/null +++ b/apps/toolbox/src/split-view/split-view-root.ts @@ -0,0 +1,17 @@ +import { Observable, EventData, Page } from '@nativescript/core'; +let page: Page; + +export function navigatingTo(args: EventData) { + 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; +} diff --git a/apps/toolbox/src/split-view/split-view-root.xml b/apps/toolbox/src/split-view/split-view-root.xml new file mode 100644 index 000000000..8cc94cf7c --- /dev/null +++ b/apps/toolbox/src/split-view/split-view-root.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/toolbox/src/split-view/split-view-secondary.ts b/apps/toolbox/src/split-view/split-view-secondary.ts new file mode 100644 index 000000000..ea9b55049 --- /dev/null +++ b/apps/toolbox/src/split-view/split-view-secondary.ts @@ -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 = 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); + } +} diff --git a/apps/toolbox/src/split-view/split-view-secondary.xml b/apps/toolbox/src/split-view/split-view-secondary.xml new file mode 100644 index 000000000..f3a31aab8 --- /dev/null +++ b/apps/toolbox/src/split-view/split-view-secondary.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/apps/toolbox/src/split-view/split-view-supplement.ts b/apps/toolbox/src/split-view/split-view-supplement.ts new file mode 100644 index 000000000..658a7b885 --- /dev/null +++ b/apps/toolbox/src/split-view/split-view-supplement.ts @@ -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 = 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); + } +} diff --git a/apps/toolbox/src/split-view/split-view-supplement.xml b/apps/toolbox/src/split-view/split-view-supplement.xml new file mode 100644 index 000000000..03a99b1b3 --- /dev/null +++ b/apps/toolbox/src/split-view/split-view-supplement.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/core/ui/index.ts b/packages/core/ui/index.ts index eb100ec06..df465b9b3 100644 --- a/packages/core/ui/index.ts +++ b/packages/core/ui/index.ts @@ -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'; diff --git a/packages/core/ui/split-view/index.android.ts b/packages/core/ui/split-view/index.android.ts new file mode 100644 index 000000000..74165742e --- /dev/null +++ b/packages/core/ui/split-view/index.android.ts @@ -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. +} diff --git a/packages/core/ui/split-view/index.d.ts b/packages/core/ui/split-view/index.d.ts new file mode 100644 index 000000000..25382fbcf --- /dev/null +++ b/packages/core/ui/split-view/index.d.ts @@ -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 {} diff --git a/packages/core/ui/split-view/index.ios.ts b/packages/core/ui/split-view/index.ios.ts new file mode 100644 index 000000000..5cb003f4c --- /dev/null +++ b/packages/core/ui/split-view/index.ios.ts @@ -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; + + public static initWithOwner(owner: WeakRef): UISplitViewControllerDelegateImpl { + const d = 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(); + private _children = new Map(); + + 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(); + } +} diff --git a/packages/core/ui/split-view/split-view-common.ts b/packages/core/ui/split-view/split-view-common.ts new file mode 100644 index 000000000..e4020d787 --- /dev/null +++ b/packages/core/ui/split-view/split-view-common.ts @@ -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(makeValidator('primary', 'secondary', 'supplementary')); + +export type SplitStyle = 'automatic' | 'double' | 'triple'; + +export type SplitDisplayMode = 'automatic' | 'secondaryOnly' | 'oneBesideSecondary' | 'oneOverSecondary' | 'twoBesideSecondary' | 'twoOverSecondary' | 'twoDisplaceSecondary'; +const splitDisplayModeConverter = makeParser(makeValidator('automatic', 'secondaryOnly', 'oneBesideSecondary', 'oneOverSecondary', 'twoBesideSecondary', 'twoOverSecondary', 'twoDisplaceSecondary')); + +export type SplitBehavior = 'automatic' | 'tile' | 'overlay' | 'displace'; +const splitBehaviorConverter = makeParser(makeValidator('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({ + 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({ + name: 'displayMode', + defaultValue: 'automatic', + affectsLayout: __APPLE__, + valueConverter: splitDisplayModeConverter, +}); +displayModeProperty.register(SplitViewBase); + +export const splitBehaviorProperty = new Property({ + name: 'splitBehavior', + defaultValue: 'automatic', + affectsLayout: __APPLE__, + valueConverter: splitBehaviorConverter, +}); +splitBehaviorProperty.register(SplitViewBase); + +export const preferredPrimaryColumnWidthFractionProperty = new Property({ + name: 'preferredPrimaryColumnWidthFraction', + defaultValue: 0, + affectsLayout: __APPLE__, + valueConverter: (v) => Math.max(0, Math.min(1, parseFloat(v))), +}); +preferredPrimaryColumnWidthFractionProperty.register(SplitViewBase); + +export const preferredSupplementaryColumnWidthFractionProperty = new Property({ + name: 'preferredSupplementaryColumnWidthFraction', + defaultValue: 0, + affectsLayout: __APPLE__, + valueConverter: (v) => Math.max(0, Math.min(1, parseFloat(v))), +}); +preferredSupplementaryColumnWidthFractionProperty.register(SplitViewBase);