diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs index e6d066b2f..081401204 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs @@ -83,6 +83,7 @@ internal override IEnumerable GetNavItems() new (){ Id = "522", Text = "Script Loader", Href = DemoRouteConstants.Demos_URL_ScriptLoader, IconName = IconName.CodeSlash, ParentId = "5" }, new (){ Id = "523", Text = "Sidebar", Href = DemoRouteConstants.Demos_URL_Sidebar, IconName = IconName.LayoutSidebar, ParentId = "5" }, new (){ Id = "524", Text = "Sidebar 2", Href = DemoRouteConstants.Demos_URL_Sidebar2, IconName = IconName.ListNested, ParentId = "5" }, + new (){ Id = "5245", Text = "Split View", Href = DemoRouteConstants.Demos_URL_SplitView, IconName = IconName.LayoutSplit, ParentId = "5" }, new (){ Id = "525", Text = "Sortable List", Href = DemoRouteConstants.Demos_URL_SortableList, IconName = IconName.ArrowsMove, ParentId = "5" }, new (){ Id = "526", Text = "Spinner", Href = DemoRouteConstants.Demos_URL_Spinners, IconName = IconName.ArrowRepeat, ParentId = "5" }, new (){ Id = "527", Text = "Tabs", Href = DemoRouteConstants.Demos_URL_Tabs, IconName = IconName.WindowPlus, ParentId = "5" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs index d05756698..fed5c4c02 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs @@ -59,6 +59,7 @@ internal override IEnumerable GetNavItems() new (){ Id = "522", Text = "Script Loader", Href = DemoRouteConstants.Docs_URL_ScriptLoader, IconName = IconName.CodeSlash, ParentId = "5" }, new (){ Id = "523", Text = "Sidebar", Href = DemoRouteConstants.Docs_URL_Sidebar, IconName = IconName.LayoutSidebar, ParentId = "5" }, new (){ Id = "524", Text = "Sidebar 2", Href = DemoRouteConstants.Docs_URL_Sidebar2, IconName = IconName.ListNested, ParentId = "5" }, + new (){ Id = "5245", Text = "Split View", Href = DemoRouteConstants.Docs_URL_SplitView, IconName = IconName.LayoutSplit, ParentId = "5" }, new (){ Id = "525", Text = "Sortable List", Href = DemoRouteConstants.Docs_URL_SortableList, IconName = IconName.ArrowsMove, ParentId = "5" }, new (){ Id = "526", Text = "Spinner", Href = DemoRouteConstants.Docs_URL_Spinners, IconName = IconName.ArrowRepeat, ParentId = "5" }, new (){ Id = "527", Text = "Tabs", Href = DemoRouteConstants.Docs_URL_Tabs, IconName = IconName.WindowPlus, ParentId = "5" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs index a88f8c744..f20977c51 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs @@ -82,6 +82,7 @@ internal override IEnumerable GetNavItems() new (){ Id = "522", Text = "Script Loader", Href = DemoRouteConstants.Demos_URL_ScriptLoader, IconName = IconName.CodeSlash, ParentId = "5" }, new (){ Id = "523", Text = "Sidebar", Href = DemoRouteConstants.Demos_URL_Sidebar, IconName = IconName.LayoutSidebar, ParentId = "5" }, new (){ Id = "524", Text = "Sidebar 2", Href = DemoRouteConstants.Demos_URL_Sidebar2, IconName = IconName.ListNested, ParentId = "5" }, + new (){ Id = "5245", Text = "Split View", Href = DemoRouteConstants.Demos_URL_SplitView, IconName = IconName.LayoutSplit, ParentId = "5" }, new (){ Id = "525", Text = "Sortable List", Href = DemoRouteConstants.Demos_URL_SortableList, IconName = IconName.ArrowsMove, ParentId = "5" }, new (){ Id = "526", Text = "Spinner", Href = DemoRouteConstants.Demos_URL_Spinners, IconName = IconName.ArrowRepeat, ParentId = "5" }, new (){ Id = "527", Text = "Tabs", Href = DemoRouteConstants.Demos_URL_Tabs, IconName = IconName.WindowPlus, ParentId = "5" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitViewDocumentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitViewDocumentation.razor new file mode 100644 index 000000000..df6b46b19 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitViewDocumentation.razor @@ -0,0 +1,104 @@ +@attribute [Route(pageUrl)] +@layout DemosMainLayout + + + + + +
+
+ Pane1 and Pane2 are the required content regions for SplitView. +
+ +
+ +
+
+ Set Orientation="SplitViewOrientation.Vertical" to stack panes top to bottom with a horizontal divider. +
+ +
+ +
+
+ Use PrimaryPaneSize to set the initial percentage allocated to the primary pane. +
+ +
+ +
+
+ Use @@bind-PrimaryPaneSize or PrimaryPaneSizeChanged to react to live divider movement. +
+ +
+ +
+
+ MinimumPaneSize clamps both panes so one side cannot fully collapse while dragging. +
+ +
+ +
+
+ Set IsDisabled="true" to keep the layout visible while preventing pointer-based resizing. +
+ +
+ +
+
+ The Color parameter uses the SplitViewColor enum and maps to shared Bootstrap-backed CSS variables. +
+ +
+ +
+
+ Use CustomColor when you want to pass a CSS color expression such as var(...) or rgba(...). +
+ +
+ +
+
+ OnResizeStarted fires once when the pointer first captures the divider. +
+ +
+ +
+
+ OnResized streams live primary and secondary pane percentages during drag. +
+ +
+ +
+
+ OnResizeEnded is useful when you only want to persist the final pane sizes after dragging stops. +
+ +
+ +
+
+ SplitView supports nested layouts, enabling IDE-style and dashboard-style workspaces. +
+ +
+ +@code { + private const string pageUrl = DemoRouteConstants.Demos_URL_SplitView; + private const string pageTitle = "Blazor Split View"; + private const string pageDescription = "The Blazor Split View component arranges two resizable panes horizontally or vertically with live drag updates, Bootstrap color theming, and focused parameter demos."; + private const string metaTitle = "Blazor Split View Component"; + private const string metaDescription = "The Blazor Split View component arranges two resizable panes horizontally or vertically with live drag updates, Bootstrap color theming, and focused parameter demos."; + private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_SplitView; +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_01_Pane_Content.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_01_Pane_Content.razor new file mode 100644 index 000000000..5be3fff26 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_01_Pane_Content.razor @@ -0,0 +1,22 @@ + + +
+
Pane1
+
Navigation
+
+
Overview
+
Customers
+
Orders
+
Reports
+
+
+
+ +
+
Pane2
+

Workspace

+

Use the two required pane templates to define the resizable primary and secondary regions.

+
+
+
\ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_02_Orientation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_02_Orientation.razor new file mode 100644 index 000000000..504b646a7 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_02_Orientation.razor @@ -0,0 +1,48 @@ +
Horizontal Orientation
+ + +
+
Left Pane
+
Navigation
+

The default horizontal orientation places panes side by side with a vertical divider.

+
+
+ +
+
Right Pane
+
Workspace
+
    +
  • Dashboard widgets
  • +
  • Tables and forms
  • +
  • Contextual details
  • +
+
+
+
+ +
Vertical Orientation
+ + +
+
Top Pane
+
Summary
+

Vertical orientation stacks panes and changes the divider to horizontal dragging.

+
+
+ +
+
Bottom Pane
+
Details
+
    +
  • Release notes
  • +
  • Deployment checklist
  • +
  • Preview output
  • +
+
+
+
\ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_03_Primary_Pane_Size.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_03_Primary_Pane_Size.razor new file mode 100644 index 000000000..77ff83788 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_03_Primary_Pane_Size.razor @@ -0,0 +1,18 @@ + + +
+
Primary Pane
+
30%
+

Set PrimaryPaneSize when you want the initial layout to favor one pane.

+
+
+ +
+
Secondary Pane
+
70%
+

The remaining space is automatically assigned to the secondary pane.

+
+
+
\ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_04_Primary_Pane_Size_Changed.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_04_Primary_Pane_Size_Changed.razor new file mode 100644 index 000000000..0a51510ff --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_04_Primary_Pane_Size_Changed.razor @@ -0,0 +1,30 @@ +
+ + + +
Bound primary pane size: @primaryPaneSize.ToString("0.##")%
+
+ + + +
+
Primary Pane
+
Two-way Binding
+

Drag the divider or use the buttons above to update the same bound value.

+
+
+ +
+
Secondary Pane
+
Live Value
+

The parent component stays synchronized through PrimaryPaneSizeChanged.

+
+
+
+ +@code { + private double primaryPaneSize = 40; +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_05_Minimum_Pane_Size.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_05_Minimum_Pane_Size.razor new file mode 100644 index 000000000..b76b20c9a --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_05_Minimum_Pane_Size.razor @@ -0,0 +1,19 @@ + + +
+
MinimumPaneSize
+
25%
+

The divider cannot move past the 25% threshold, so the primary pane always remains usable.

+
+
+ +
+
Secondary Pane
+
Protected Layout
+

The same minimum is enforced from the opposite side, keeping both panes visible.

+
+
+
\ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_06_Is_Disabled.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_06_Is_Disabled.razor new file mode 100644 index 000000000..02363f828 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_06_Is_Disabled.razor @@ -0,0 +1,20 @@ + + +
+
Disabled Divider
+
Pinned Pane
+

The layout remains visible, but pointer dragging and hover states are disabled.

+
+
+ +
+
Secondary Pane
+
Static Layout
+

Programmatic updates to PrimaryPaneSize still work even when interaction is disabled.

+
+
+
\ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_07_Color.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_07_Color.razor new file mode 100644 index 000000000..b32416992 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_07_Color.razor @@ -0,0 +1,50 @@ +
+
+ + +
+
Primary
+
Bootstrap semantic color
+
+
+ +
Hover and dragging derive from the same base divider color.
+
+
+
+
+ + +
+
Success
+
Shared CSS variable mapping
+
+
+ +
Use enum values when you want Bootstrap-aligned theming.
+
+
+
+
+ + +
+
Dark
+
Theme-aware shared CSS
+
+
+ +
The divider color comes from the shared blazor.bootstrap.css token mapping.
+
+
+
+
\ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_08_Custom_Color.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_08_Custom_Color.razor new file mode 100644 index 000000000..bd377a07b --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_08_Custom_Color.razor @@ -0,0 +1,19 @@ + + +
+
CustomColor
+
CSS Expression Override
+

This example uses rgba(var(--bs-info-rgb), 0.85) instead of a semantic enum value.

+
+
+ +
+
Override Precedence
+
Inline Custom Value
+

When both are supplied, CustomColor overrides the shared default and semantic enum class.

+
+
+
\ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_09_On_Resize_Started.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_09_On_Resize_Started.razor new file mode 100644 index 000000000..8debf5887 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_09_On_Resize_Started.razor @@ -0,0 +1,44 @@ +
+
+ + +
+
OnResizeStarted
+
Drag To Start
+

Grab the divider to trigger the start event once per interaction.

+
+
+ +
+
Secondary Pane
+
Event Snapshot
+

The event args include both pane sizes as percentages.

+
+
+
+
+
+
+
Start Event
+
Count: @count
+
Primary pane: @primaryPaneSize.ToString("0.##")%
+
Secondary pane: @secondaryPaneSize.ToString("0.##")%
+
+
+
+ +@code { + private int count; + private double primaryPaneSize = 38; + private double secondaryPaneSize = 62; + + private void HandleResizeStarted(SplitViewResizeEventArgs args) + { + count++; + primaryPaneSize = args.PrimaryPaneSize; + secondaryPaneSize = args.SecondaryPaneSize; + } +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_10_On_Resized.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_10_On_Resized.razor new file mode 100644 index 000000000..064c27b2c --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_10_On_Resized.razor @@ -0,0 +1,41 @@ +
+
+ + +
+
OnResized
+
Live Resize Stream
+

Drag the divider and watch the event payload update continuously.

+
+
+ +
+
Live Metrics
+
Current Split
+

Primary pane: @primaryPaneSize.ToString("0.##")%

+
+
+
+
+
+
+
Resized Event
+
Primary pane: @primaryPaneSize.ToString("0.##")%
+
Secondary pane: @secondaryPaneSize.ToString("0.##")%
+
+
+
+ +@code { + private double primaryPaneSize = 42; + private double secondaryPaneSize = 58; + + private void HandleResized(SplitViewResizeEventArgs args) + { + primaryPaneSize = args.PrimaryPaneSize; + secondaryPaneSize = args.SecondaryPaneSize; + } +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_11_On_Resize_Ended.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_11_On_Resize_Ended.razor new file mode 100644 index 000000000..ffab9c544 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_11_On_Resize_Ended.razor @@ -0,0 +1,41 @@ +
+
+ + +
+
OnResizeEnded
+
Persist Final Size
+

This event fires once after dragging finishes, which is useful for saving layout preferences.

+
+
+ +
+
Secondary Pane
+
Release The Divider
+

Only the final percentages are recorded here.

+
+
+
+
+
+
+
Resize Ended
+
Primary pane: @primaryPaneSize.ToString("0.##")%
+
Secondary pane: @secondaryPaneSize.ToString("0.##")%
+
+
+
+ +@code { + private double primaryPaneSize = 36; + private double secondaryPaneSize = 64; + + private void HandleResizeEnded(SplitViewResizeEventArgs args) + { + primaryPaneSize = args.PrimaryPaneSize; + secondaryPaneSize = args.SecondaryPaneSize; + } +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_12_Nested_Layouts.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_12_Nested_Layouts.razor new file mode 100644 index 000000000..9245a0c42 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_12_Nested_Layouts.razor @@ -0,0 +1,43 @@ + + +
+
Explorer
+
Files
+
+
Pages
+
Components
+
Styles
+
Assets
+
+
+
+ + + +
+
Editor
+
SplitView.razor
+
<SplitView @@bind-PrimaryPaneSize="paneSize">
+    <Pane1>...</Pane1>
+    <Pane2>...</Pane2>
+</SplitView>
+
+
+ +
+
Preview
+
Live Preview
+

Nested SplitView layouts are useful for IDEs, dashboards, and administration portals.

+
+
+
+
+
\ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/SplitView/SplitView_Doc_01_Documentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/SplitView/SplitView_Doc_01_Documentation.razor new file mode 100644 index 000000000..332d836d1 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/SplitView/SplitView_Doc_01_Documentation.razor @@ -0,0 +1,45 @@ +@attribute [Route(pageUrl)] +@layout DocsMainLayout + + + + + +
+ @metaTitle +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +@code { + private const string componentName = nameof(SplitView); + private const string pageUrl = DemoRouteConstants.Docs_URL_SplitView; + private const string pageTitle = componentName; + private const string pageDescription = $"This documentation provides a comprehensive reference for the {componentName} component, guiding you through its configuration options."; + private const string metaTitle = $"Blazor {componentName} Component"; + private const string metaDescription = $"This documentation provides a comprehensive reference for the {componentName} component, guiding you through its configuration options."; + private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_SplitView; +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor index b4a94d512..75f780ea2 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor @@ -230,6 +230,11 @@

Sidebar 2 Updated

+

Sortable List

diff --git a/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs b/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs index e4b1a7138..9df346203 100644 --- a/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs +++ b/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs @@ -90,6 +90,7 @@ public static class DemoRouteConstants public const string Demos_URL_ScriptLoader = Demos_URL_Prefix + "/script-loader"; public const string Demos_URL_Sidebar = Demos_URL_Prefix + "/sidebar"; public const string Demos_URL_Sidebar2 = Demos_URL_Prefix + "/sidebar2"; + public const string Demos_URL_SplitView = Demos_URL_Prefix + "/split-view"; public const string Demos_URL_SortableList = Demos_URL_Prefix + "/sortable-list"; public const string Demos_URL_Spinners = Demos_URL_Prefix + "/spinners"; public const string Demos_URL_Tabs = Demos_URL_Prefix + "/tabs"; @@ -181,6 +182,7 @@ public static class DemoRouteConstants public const string Docs_URL_ScriptLoader = Docs_URL_Prefix + "/script-loader"; public const string Docs_URL_Sidebar = Docs_URL_Prefix + "/sidebar"; public const string Docs_URL_Sidebar2 = Docs_URL_Prefix + "/sidebar2"; + public const string Docs_URL_SplitView = Docs_URL_Prefix + "/split-view"; public const string Docs_URL_SortableList = Docs_URL_Prefix + "/sortable-list"; public const string Docs_URL_Spinners = Docs_URL_Prefix + "/spinners"; public const string Docs_URL_Tabs = Docs_URL_Prefix + "/tabs"; diff --git a/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs b/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs index 3db0516fe..67d975ea7 100644 --- a/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs +++ b/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs @@ -28,7 +28,7 @@ public class DemoScreenshotSrcConstants public const string Demos_URL_PasswordInput = DemoScreenshotSrcPrefix + "password-input.png"; public const string Demos_URL_RadioInput = DemoScreenshotSrcPrefix + "radio-input.png"; public const string Demos_URL_RangeInput = DemoScreenshotSrcPrefix + "range-input.png"; - public const string Demos_URL_SelectInput = DemoScreenshotSrcPrefix + "home.png"; + public const string Demos_URL_SelectInput = DemoScreenshotSrcPrefix + "select-input.png"; public const string Demos_URL_Switch = DemoScreenshotSrcPrefix + "switch.png"; public const string Demos_URL_TextAreaInput = DemoScreenshotSrcPrefix + "textarea-input.png"; public const string Demos_URL_TextInput = DemoScreenshotSrcPrefix + "text-input.png"; @@ -82,6 +82,7 @@ public class DemoScreenshotSrcConstants public const string Demos_URL_ScriptLoader = DemoScreenshotSrcPrefix + "script-loader.png"; public const string Demos_URL_Sidebar = DemoScreenshotSrcPrefix + "sidebar.png"; public const string Demos_URL_Sidebar2 = DemoScreenshotSrcPrefix + "sidebar2.png"; + public const string Demos_URL_SplitView = DemoScreenshotSrcPrefix + "split-view.png"; public const string Demos_URL_SortableList = DemoScreenshotSrcPrefix + "sortable-list.png"; public const string Demos_URL_Spinners = DemoScreenshotSrcPrefix + "spinners.png"; public const string Demos_URL_Tabs = DemoScreenshotSrcPrefix + "tabs.png"; diff --git a/BlazorBootstrap.Demo.RCL/wwwroot/images/screenshots/split-view.png b/BlazorBootstrap.Demo.RCL/wwwroot/images/screenshots/split-view.png new file mode 100644 index 000000000..5b7cc12b7 Binary files /dev/null and b/BlazorBootstrap.Demo.RCL/wwwroot/images/screenshots/split-view.png differ diff --git a/blazorbootstrap/Components/SplitView/SplitView.razor b/blazorbootstrap/Components/SplitView/SplitView.razor new file mode 100644 index 000000000..d70058741 --- /dev/null +++ b/blazorbootstrap/Components/SplitView/SplitView.razor @@ -0,0 +1,14 @@ +@namespace BlazorBootstrap +@inherits BlazorBootstrapComponentBase + +
+
+ @Pane1 +
+ +
+ @Pane2 +
+
\ No newline at end of file diff --git a/blazorbootstrap/Components/SplitView/SplitView.razor.cs b/blazorbootstrap/Components/SplitView/SplitView.razor.cs new file mode 100644 index 000000000..739d8c093 --- /dev/null +++ b/blazorbootstrap/Components/SplitView/SplitView.razor.cs @@ -0,0 +1,304 @@ +namespace BlazorBootstrap; + +public partial class SplitView : BlazorBootstrapComponentBase +{ + #region Fields and Constants + + private double currentPrimaryPaneSize = 50; + private bool hasReceivedParameters; + private bool isResizing; + private DotNetObjectReference? objRef; + private SplitViewColor previousColor; + private string? previousCustomColor; + private double previousMinimumPaneSizeParameter; + private double previousPrimaryPaneSizeParameter; + private double previousMinimumPaneSize; + private SplitViewOrientation previousOrientation; + private double previousPrimaryPaneSize; + private bool previousIsDisabled; + + #endregion + + #region Methods + + /// + protected override async ValueTask DisposeAsyncCore(bool disposing) + { + if (disposing) + { + try + { + if (IsRenderComplete && Id is not null) + await SafeInvokeVoidAsync("window.blazorBootstrap.splitView.dispose", Id); + } + catch (JSDisconnectedException) + { + // do nothing + } + + objRef?.Dispose(); + } + + await base.DisposeAsyncCore(disposing); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await SafeInvokeVoidAsync("window.blazorBootstrap.splitView.initialize", Id!, Orientation.ToString(), currentPrimaryPaneSize, EffectiveMinimumPaneSize, + IsDisabled, objRef!); + + CaptureRenderedState(); + } + else if (!isResizing && ParametersChanged()) + { + await SafeInvokeVoidAsync("window.blazorBootstrap.splitView.update", Id!, Orientation.ToString(), currentPrimaryPaneSize, EffectiveMinimumPaneSize, + IsDisabled); + + CaptureRenderedState(); + } + + await base.OnAfterRenderAsync(firstRender); + } + + protected override Task OnInitializedAsync() + { + objRef ??= DotNetObjectReference.Create(this); + + return base.OnInitializedAsync(); + } + + protected override Task OnParametersSetAsync() + { + var normalizedPrimaryPaneSize = NormalizePrimaryPaneSize(PrimaryPaneSize); + var primaryPaneSizeParameterChanged = !hasReceivedParameters || Math.Abs(previousPrimaryPaneSizeParameter - PrimaryPaneSize) >= 0.01d; + var minimumPaneSizeParameterChanged = !hasReceivedParameters || Math.Abs(previousMinimumPaneSizeParameter - MinimumPaneSize) >= 0.01d; + + if (primaryPaneSizeParameterChanged) + currentPrimaryPaneSize = normalizedPrimaryPaneSize; + else if (minimumPaneSizeParameterChanged) + currentPrimaryPaneSize = NormalizePrimaryPaneSize(currentPrimaryPaneSize); + + hasReceivedParameters = true; + previousPrimaryPaneSizeParameter = PrimaryPaneSize; + previousMinimumPaneSizeParameter = MinimumPaneSize; + + return base.OnParametersSetAsync(); + } + + [JSInvokable] + public async Task OnResizeEndedJS(double primaryPaneSize, double secondaryPaneSize) + { + isResizing = false; + currentPrimaryPaneSize = primaryPaneSize; + CaptureRenderedState(); + + if (OnResizeEnded.HasDelegate) + await OnResizeEnded.InvokeAsync(new SplitViewResizeEventArgs(primaryPaneSize, secondaryPaneSize, Orientation)); + } + + [JSInvokable] + public async Task OnResizeStartedJS(double primaryPaneSize, double secondaryPaneSize) + { + isResizing = true; + currentPrimaryPaneSize = primaryPaneSize; + + if (OnResizeStarted.HasDelegate) + await OnResizeStarted.InvokeAsync(new SplitViewResizeEventArgs(primaryPaneSize, secondaryPaneSize, Orientation)); + } + + [JSInvokable] + public async Task OnResizedJS(double primaryPaneSize, double secondaryPaneSize) + { + if (Math.Abs(currentPrimaryPaneSize - primaryPaneSize) < 0.01d && !PrimaryPaneSizeChanged.HasDelegate && !OnResized.HasDelegate) + return; + + currentPrimaryPaneSize = primaryPaneSize; + + if (PrimaryPaneSizeChanged.HasDelegate) + await PrimaryPaneSizeChanged.InvokeAsync(primaryPaneSize); + + if (OnResized.HasDelegate) + await OnResized.InvokeAsync(new SplitViewResizeEventArgs(primaryPaneSize, secondaryPaneSize, Orientation)); + } + + private void CaptureRenderedState() + { + previousPrimaryPaneSize = currentPrimaryPaneSize; + previousMinimumPaneSize = EffectiveMinimumPaneSize; + previousOrientation = Orientation; + previousIsDisabled = IsDisabled; + previousColor = Color; + previousCustomColor = CustomColor; + } + + private double NormalizePrimaryPaneSize(double primaryPaneSize) + { + var minimumPaneSize = EffectiveMinimumPaneSize; + var maximumPaneSize = 100d - minimumPaneSize; + + return Math.Clamp(primaryPaneSize, minimumPaneSize, maximumPaneSize); + } + + private bool ParametersChanged() => + previousPrimaryPaneSize != currentPrimaryPaneSize || + previousMinimumPaneSize != EffectiveMinimumPaneSize || + previousOrientation != Orientation || + previousIsDisabled != IsDisabled || + previousColor != Color || + previousCustomColor != CustomColor; + + #endregion + + #region Properties, Indexers + + protected override string? ClassNames => + BuildClassNames(Class, + ("bb-split-view", true), + ("bb-split-view-horizontal", Orientation == SplitViewOrientation.Horizontal), + ("bb-split-view-vertical", Orientation == SplitViewOrientation.Vertical), + (Color.ToSplitViewColorClass(), Color != SplitViewColor.None), + ("bb-split-view-disabled", IsDisabled)); + + private double EffectiveMinimumPaneSize => Math.Clamp(MinimumPaneSize, 0d, 50d); + + /// + /// Gets or sets the divider color. + /// + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(SplitViewColor.None)] + [Description("Gets or sets the divider color.")] + [Parameter] + public SplitViewColor Color { get; set; } + + /// + /// Gets or sets a custom divider color. + /// + /// Accepts any valid CSS color expression, including CSS variables. + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(null)] + [Description("Gets or sets a custom divider color.")] + [Parameter] + public string? CustomColor { get; set; } + + /// + /// Gets or sets a value indicating whether user interaction is disabled. + /// + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(false)] + [Description("Gets or sets a value indicating whether user interaction is disabled.")] + [Parameter] + public bool IsDisabled { get; set; } + + /// + /// Gets or sets the minimum pane size as a percentage. + /// + /// Default value is 0. + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(0d)] + [Description("Gets or sets the minimum pane size as a percentage.")] + [Parameter] + public double MinimumPaneSize { get; set; } + + /// + /// Fired when resizing ends. + /// + [AddedVersion("4.0.0")] + [Description("Fired when resizing ends.")] + [Parameter] + public EventCallback OnResizeEnded { get; set; } + + /// + /// Fired while the divider is being dragged. + /// + [AddedVersion("4.0.0")] + [Description("Fired while the divider is being dragged.")] + [Parameter] + public EventCallback OnResized { get; set; } + + /// + /// Fired when resizing starts. + /// + [AddedVersion("4.0.0")] + [Description("Fired when resizing starts.")] + [Parameter] + public EventCallback OnResizeStarted { get; set; } + + /// + /// Gets or sets the SplitView orientation. + /// + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(SplitViewOrientation.Horizontal)] + [Description("Gets or sets the SplitView orientation.")] + [Parameter] + public SplitViewOrientation Orientation { get; set; } = SplitViewOrientation.Horizontal; + + /// + /// Gets or sets the first pane content. + /// + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(null)] + [Description("Gets or sets the first pane content.")] + [EditorRequired] + [Parameter] + public RenderFragment? Pane1 { get; set; } + + /// + /// Gets or sets the second pane content. + /// + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(null)] + [Description("Gets or sets the second pane content.")] + [EditorRequired] + [Parameter] + public RenderFragment? Pane2 { get; set; } + + /// + /// Gets or sets the primary pane size as a percentage. + /// + /// Default value is 50. + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(50d)] + [Description("Gets or sets the primary pane size as a percentage.")] + [Parameter] + public double PrimaryPaneSize { get; set; } = 50d; + + /// + /// Fired when the primary pane size changes. + /// + [AddedVersion("4.0.0")] + [Description("Fired when the primary pane size changes.")] + [Parameter] + public EventCallback PrimaryPaneSizeChanged { get; set; } + + private string SeparatorAriaOrientation => Orientation == SplitViewOrientation.Horizontal ? "vertical" : "horizontal"; + + protected override string? StyleNames => + BuildStyleNames(Style, + ($"--bb-split-view-divider-color:{CustomColor}", !string.IsNullOrWhiteSpace(CustomColor))); + + #endregion +} \ No newline at end of file diff --git a/blazorbootstrap/Enums/Color/SplitViewColor.cs b/blazorbootstrap/Enums/Color/SplitViewColor.cs new file mode 100644 index 000000000..ef94f22d0 --- /dev/null +++ b/blazorbootstrap/Enums/Color/SplitViewColor.cs @@ -0,0 +1,112 @@ +namespace BlazorBootstrap; + +/// +/// Defines the supported visual color options for a BlazorBootstrap split view divider. +/// +public enum SplitViewColor +{ + /// + /// Indicates that no explicit color option is selected. + /// + [AddedVersion("4.0.0")] + [Description("Indicates that no explicit color option is selected.")] + None, + + /// + /// Indicates the primary (emphasis) color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates the primary (emphasis) color option.")] + Primary, + + /// + /// Indicates the secondary (less emphasized) color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates the secondary (less emphasized) color option.")] + Secondary, + + /// + /// Indicates a success-state color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates a success-state color option.")] + Success, + + /// + /// Indicates an error or danger-state color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates an error or danger-state color option.")] + Danger, + + /// + /// Indicates a warning-state color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates a warning-state color option.")] + Warning, + + /// + /// Indicates an informational-state color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates an informational-state color option.")] + Info, + + /// + /// Indicates a light-toned color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates a light-toned color option.")] + Light, + + /// + /// Indicates a dark-toned color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates a dark-toned color option.")] + Dark, + + /// + /// Indicates the body background color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates the body background color option.")] + Body, + + /// + /// Indicates the secondary body background color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates the secondary body background color option.")] + BodySecondary, + + /// + /// Indicates the tertiary body background color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates the tertiary body background color option.")] + BodyTertiary, + + /// + /// Indicates the black color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates the black color option.")] + Black, + + /// + /// Indicates the white color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates the white color option.")] + White, + + /// + /// Indicates a transparent color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates a transparent color option.")] + Transparent +} \ No newline at end of file diff --git a/blazorbootstrap/Enums/SplitViewOrientation.cs b/blazorbootstrap/Enums/SplitViewOrientation.cs new file mode 100644 index 000000000..d4c74da39 --- /dev/null +++ b/blazorbootstrap/Enums/SplitViewOrientation.cs @@ -0,0 +1,21 @@ +namespace BlazorBootstrap; + +/// +/// Defines the available layout orientations for the component. +/// +public enum SplitViewOrientation +{ + /// + /// Arranges panes side by side with a vertical divider. + /// + [AddedVersion("4.0.0")] + [Description("Arranges panes side by side with a vertical divider.")] + Horizontal, + + /// + /// Arranges panes top to bottom with a horizontal divider. + /// + [AddedVersion("4.0.0")] + [Description("Arranges panes top to bottom with a horizontal divider.")] + Vertical, +} \ No newline at end of file diff --git a/blazorbootstrap/EventArguments/SplitViewResizeEventArgs.cs b/blazorbootstrap/EventArguments/SplitViewResizeEventArgs.cs new file mode 100644 index 000000000..a5dd6fdec --- /dev/null +++ b/blazorbootstrap/EventArguments/SplitViewResizeEventArgs.cs @@ -0,0 +1,37 @@ +namespace BlazorBootstrap; + +/// +/// Provides resize data for the component. +/// +public class SplitViewResizeEventArgs : EventArgs +{ + #region Constructors + + public SplitViewResizeEventArgs(double primaryPaneSize, double secondaryPaneSize, SplitViewOrientation orientation) + { + PrimaryPaneSize = primaryPaneSize; + SecondaryPaneSize = secondaryPaneSize; + Orientation = orientation; + } + + #endregion + + #region Properties, Indexers + + /// + /// Gets the current orientation. + /// + public SplitViewOrientation Orientation { get; } + + /// + /// Gets the primary pane size as a percentage. + /// + public double PrimaryPaneSize { get; } + + /// + /// Gets the secondary pane size as a percentage. + /// + public double SecondaryPaneSize { get; } + + #endregion +} \ No newline at end of file diff --git a/blazorbootstrap/Extensions/EnumExtensions.cs b/blazorbootstrap/Extensions/EnumExtensions.cs index a05d02c87..d36cfec3a 100644 --- a/blazorbootstrap/Extensions/EnumExtensions.cs +++ b/blazorbootstrap/Extensions/EnumExtensions.cs @@ -65,6 +65,26 @@ public static string ToBackgroundClass(this BackgroundColor backgroundColor) => _ => "" }; + public static string ToSplitViewColorClass(this SplitViewColor splitViewColor) => + splitViewColor switch + { + SplitViewColor.Primary => "bb-split-view-color-primary", + SplitViewColor.Secondary => "bb-split-view-color-secondary", + SplitViewColor.Success => "bb-split-view-color-success", + SplitViewColor.Danger => "bb-split-view-color-danger", + SplitViewColor.Warning => "bb-split-view-color-warning", + SplitViewColor.Info => "bb-split-view-color-info", + SplitViewColor.Light => "bb-split-view-color-light", + SplitViewColor.Dark => "bb-split-view-color-dark", + SplitViewColor.Body => "bb-split-view-color-body", + SplitViewColor.BodySecondary => "bb-split-view-color-body-secondary", + SplitViewColor.BodyTertiary => "bb-split-view-color-body-tertiary", + SplitViewColor.Black => "bb-split-view-color-black", + SplitViewColor.White => "bb-split-view-color-white", + SplitViewColor.Transparent => "bb-split-view-color-transparent", + _ => "" + }; + public static string ToBadgeColorClass(this BadgeColor badgeColor) => badgeColor switch { diff --git a/blazorbootstrap/wwwroot/blazor.bootstrap.css b/blazorbootstrap/wwwroot/blazor.bootstrap.css index 5f2b4d32d..0e43e9aad 100644 --- a/blazorbootstrap/wwwroot/blazor.bootstrap.css +++ b/blazorbootstrap/wwwroot/blazor.bootstrap.css @@ -22,6 +22,29 @@ --bb-tooltip-dark: var(--bs-dark); --bb-tooltip-color-white: var(--bs-white); --bb-tooltip-color-dark: var(--bs-black); + /* split view */ + --bb-split-view-divider-size: 0.75rem; + --bb-split-view-divider-color: var(--bs-border-color); + --bb-split-view-divider-rest-opacity: 0.65; + --bb-split-view-divider-hover-opacity: 0.8; + --bb-split-view-divider-dragging-opacity: 1; + --bb-split-view-divider-disabled-opacity: 0.45; + --bb-split-view-divider-handle-color: var(--bs-emphasis-color); + --bb-split-view-divider-handle-opacity: 0.5; + --bb-split-view-color-primary: var(--bs-primary); + --bb-split-view-color-secondary: var(--bs-secondary); + --bb-split-view-color-success: var(--bs-success); + --bb-split-view-color-danger: var(--bs-danger); + --bb-split-view-color-warning: var(--bs-warning); + --bb-split-view-color-info: var(--bs-info); + --bb-split-view-color-light: var(--bs-light); + --bb-split-view-color-dark: var(--bs-dark); + --bb-split-view-color-body: var(--bs-body-bg); + --bb-split-view-color-body-secondary: var(--bs-secondary-bg); + --bb-split-view-color-body-tertiary: var(--bs-tertiary-bg); + --bb-split-view-color-black: var(--bs-black); + --bb-split-view-color-white: var(--bs-white); + --bb-split-view-color-transparent: transparent; /* border */ --bs-border-radius-xs: 0.125rem; --bs-border-radius-md: 0.375rem; @@ -189,6 +212,156 @@ table button.dropdown-toggle.bb-grid-filter::after { padding: .375rem; } +/* split view */ +.bb-split-view { + display: flex; + min-block-size: 0; + min-inline-size: 0; + overflow: hidden; + position: relative; +} + +.bb-split-view-horizontal { + flex-direction: row; +} + +.bb-split-view-vertical { + flex-direction: column; +} + +.bb-split-view-pane { + min-block-size: 0; + min-inline-size: 0; + overflow: auto; +} + +.bb-split-view-pane-primary { + flex: 0 0 auto; +} + +.bb-split-view-pane-secondary { + flex: 1 1 auto; +} + +.bb-split-view-divider { + align-items: center; + background-color: transparent; + display: flex; + flex: 0 0 var(--bb-split-view-divider-size); + justify-content: center; + position: relative; + touch-action: none; + user-select: none; +} + +.bb-split-view-divider::before { + background-color: var(--bb-split-view-divider-color); + content: ""; + inset: 0; + opacity: var(--bb-split-view-divider-rest-opacity); + position: absolute; + transition: opacity 0.15s ease-in-out; +} + +.bb-split-view-horizontal > .bb-split-view-divider { + cursor: col-resize; +} + +.bb-split-view-vertical > .bb-split-view-divider { + cursor: row-resize; +} + +.bb-split-view:not(.bb-split-view-disabled):not(.bb-split-view-dragging) > .bb-split-view-divider:hover::before { + opacity: var(--bb-split-view-divider-hover-opacity); +} + +.bb-split-view.bb-split-view-dragging > .bb-split-view-divider::before { + opacity: var(--bb-split-view-divider-dragging-opacity); +} + +.bb-split-view-disabled > .bb-split-view-divider { + cursor: default; +} + +.bb-split-view-disabled > .bb-split-view-divider::before { + opacity: var(--bb-split-view-divider-disabled-opacity); +} + +.bb-split-view-divider-handle { + background-color: var(--bb-split-view-divider-handle-color); + border-radius: 999px; + display: block; + opacity: var(--bb-split-view-divider-handle-opacity); + position: relative; + z-index: 1; +} + +.bb-split-view-horizontal > .bb-split-view-divider > .bb-split-view-divider-handle { + height: 2rem; + width: 2px; +} + +.bb-split-view-vertical > .bb-split-view-divider > .bb-split-view-divider-handle { + height: 2px; + width: 2rem; +} + +.bb-split-view.bb-split-view-color-primary { + --bb-split-view-divider-color: var(--bb-split-view-color-primary); +} + +.bb-split-view.bb-split-view-color-secondary { + --bb-split-view-divider-color: var(--bb-split-view-color-secondary); +} + +.bb-split-view.bb-split-view-color-success { + --bb-split-view-divider-color: var(--bb-split-view-color-success); +} + +.bb-split-view.bb-split-view-color-danger { + --bb-split-view-divider-color: var(--bb-split-view-color-danger); +} + +.bb-split-view.bb-split-view-color-warning { + --bb-split-view-divider-color: var(--bb-split-view-color-warning); +} + +.bb-split-view.bb-split-view-color-info { + --bb-split-view-divider-color: var(--bb-split-view-color-info); +} + +.bb-split-view.bb-split-view-color-light { + --bb-split-view-divider-color: var(--bb-split-view-color-light); +} + +.bb-split-view.bb-split-view-color-dark { + --bb-split-view-divider-color: var(--bb-split-view-color-dark); +} + +.bb-split-view.bb-split-view-color-body { + --bb-split-view-divider-color: var(--bb-split-view-color-body); +} + +.bb-split-view.bb-split-view-color-body-secondary { + --bb-split-view-divider-color: var(--bb-split-view-color-body-secondary); +} + +.bb-split-view.bb-split-view-color-body-tertiary { + --bb-split-view-divider-color: var(--bb-split-view-color-body-tertiary); +} + +.bb-split-view.bb-split-view-color-black { + --bb-split-view-divider-color: var(--bb-split-view-color-black); +} + +.bb-split-view.bb-split-view-color-white { + --bb-split-view-divider-color: var(--bb-split-view-color-white); +} + +.bb-split-view.bb-split-view-color-transparent { + --bb-split-view-divider-color: var(--bb-split-view-color-transparent); +} + /* grid - fixed header */ .bb-table { /* NOTE: intentionally overriding the behavior */ diff --git a/blazorbootstrap/wwwroot/blazor.bootstrap.js b/blazorbootstrap/wwwroot/blazor.bootstrap.js index 7594cf3a2..9794e33cf 100644 --- a/blazorbootstrap/wwwroot/blazor.bootstrap.js +++ b/blazorbootstrap/wwwroot/blazor.bootstrap.js @@ -814,6 +814,199 @@ window.blazorBootstrap = { }, windowSize: () => window.innerWidth }, + splitView: { + applyPaneSize: (state, primaryPaneSize) => { + let normalizedPaneSize = window.blazorBootstrap.splitView.normalizePaneSize(state, primaryPaneSize); + let availableSize = window.blazorBootstrap.splitView.getAvailableSize(state); + let primaryPanePixels = availableSize * normalizedPaneSize / 100; + + state.primaryPaneSize = normalizedPaneSize; + state.primaryPane.style.flexBasis = `${primaryPanePixels}px`; + }, + clamp: (value, min, max) => Math.min(Math.max(value, min), max), + createResizeObserver: (state) => { + if (typeof ResizeObserver === 'undefined') + return null; + + let resizeObserver = new ResizeObserver(() => { + if (!state.isDragging) + window.blazorBootstrap.splitView.applyPaneSize(state, state.primaryPaneSize); + }); + + resizeObserver.observe(state.root); + + return resizeObserver; + }, + dispose: (elementId) => { + let state = window.blazorBootstrap.splitView.instances[elementId]; + if (!state) + return; + + state.divider.removeEventListener('pointerdown', state.pointerDownHandler); + state.divider.removeEventListener('pointermove', state.pointerMoveHandler); + state.divider.removeEventListener('pointerup', state.pointerUpHandler); + state.divider.removeEventListener('pointercancel', state.pointerUpHandler); + state.divider.removeEventListener('lostpointercapture', state.lostPointerCaptureHandler); + state.resizeObserver?.disconnect(); + state.root.classList.remove('bb-split-view-dragging'); + + delete window.blazorBootstrap.splitView.instances[elementId]; + }, + emitResizeEvent: (state, methodName) => { + if (!state.dotNetHelper) + return; + + let primaryPaneSize = window.blazorBootstrap.splitView.roundPaneSize(state.primaryPaneSize); + let secondaryPaneSize = window.blazorBootstrap.splitView.roundPaneSize(100 - primaryPaneSize); + + state.dotNetHelper.invokeMethodAsync(methodName, primaryPaneSize, secondaryPaneSize) + .catch(() => { + // do nothing + }); + }, + getAvailableSize: (state) => { + let rect = state.root.getBoundingClientRect(); + let dividerRect = state.divider.getBoundingClientRect(); + let dividerSize = state.orientation === 'Horizontal' ? dividerRect.width : dividerRect.height; + let totalSize = state.orientation === 'Horizontal' ? rect.width : rect.height; + + return Math.max(totalSize - dividerSize, 0); + }, + getDirectChild: (root, className) => { + let children = Array.from(root.children); + return children.find((child) => child.classList?.contains(className)) ?? null; + }, + initialize: (elementId, orientation, primaryPaneSize, minimumPaneSize, isDisabled, dotNetHelper) => { + window.blazorBootstrap.splitView.dispose(elementId); + + let root = document.getElementById(elementId); + if (!root) + return; + + let primaryPane = window.blazorBootstrap.splitView.getDirectChild(root, 'bb-split-view-pane-primary'); + let divider = window.blazorBootstrap.splitView.getDirectChild(root, 'bb-split-view-divider'); + let secondaryPane = window.blazorBootstrap.splitView.getDirectChild(root, 'bb-split-view-pane-secondary'); + + if (!primaryPane || !divider || !secondaryPane) + return; + + let state = { + activePointerId: null, + divider: divider, + dotNetHelper: dotNetHelper, + isDisabled: isDisabled, + isDragging: false, + lostPointerCaptureHandler: null, + minimumPaneSize: minimumPaneSize, + orientation: orientation, + pointerDownHandler: null, + pointerMoveHandler: null, + pointerUpHandler: null, + primaryPane: primaryPane, + primaryPaneSize: primaryPaneSize, + resizeObserver: null, + root: root, + secondaryPane: secondaryPane, + startPosition: 0, + startPrimaryPaneSize: primaryPaneSize + }; + + state.pointerDownHandler = (event) => window.blazorBootstrap.splitView.onPointerDown(state, event); + state.pointerMoveHandler = (event) => window.blazorBootstrap.splitView.onPointerMove(state, event); + state.pointerUpHandler = (event) => window.blazorBootstrap.splitView.stopDragging(state, event); + state.lostPointerCaptureHandler = (event) => window.blazorBootstrap.splitView.stopDragging(state, event); + state.resizeObserver = window.blazorBootstrap.splitView.createResizeObserver(state); + + divider.addEventListener('pointerdown', state.pointerDownHandler); + divider.addEventListener('pointermove', state.pointerMoveHandler); + divider.addEventListener('pointerup', state.pointerUpHandler); + divider.addEventListener('pointercancel', state.pointerUpHandler); + divider.addEventListener('lostpointercapture', state.lostPointerCaptureHandler); + + window.blazorBootstrap.splitView.updateState(state, orientation, primaryPaneSize, minimumPaneSize, isDisabled); + + window.blazorBootstrap.splitView.instances[elementId] = state; + }, + instances: {}, + normalizePaneSize: (state, value) => { + let minimumPaneSize = window.blazorBootstrap.splitView.clamp(state.minimumPaneSize, 0, 50); + return window.blazorBootstrap.splitView.clamp(value, minimumPaneSize, 100 - minimumPaneSize); + }, + onPointerDown: (state, event) => { + if (state.isDisabled) + return; + + if (event.pointerType === 'mouse' && event.button !== 0) + return; + + event.preventDefault(); + event.stopPropagation(); + + state.isDragging = true; + state.activePointerId = event.pointerId; + state.startPosition = state.orientation === 'Horizontal' ? event.clientX : event.clientY; + state.startPrimaryPaneSize = state.primaryPaneSize; + + state.root.classList.add('bb-split-view-dragging'); + state.divider.setPointerCapture?.(event.pointerId); + + window.blazorBootstrap.splitView.emitResizeEvent(state, 'OnResizeStartedJS'); + }, + onPointerMove: (state, event) => { + if (!state.isDragging || state.activePointerId !== event.pointerId) + return; + + event.preventDefault(); + event.stopPropagation(); + + let currentPosition = state.orientation === 'Horizontal' ? event.clientX : event.clientY; + let delta = currentPosition - state.startPosition; + let availableSize = window.blazorBootstrap.splitView.getAvailableSize(state); + + if (availableSize <= 0) + return; + + let nextPaneSize = state.startPrimaryPaneSize + (delta / availableSize * 100); + let normalizedPaneSize = window.blazorBootstrap.splitView.normalizePaneSize(state, nextPaneSize); + + if (Math.abs(normalizedPaneSize - state.primaryPaneSize) < 0.01) + return; + + window.blazorBootstrap.splitView.applyPaneSize(state, normalizedPaneSize); + window.blazorBootstrap.splitView.emitResizeEvent(state, 'OnResizedJS'); + }, + roundPaneSize: (value) => Math.round(value * 100) / 100, + stopDragging: (state, event) => { + if (!state.isDragging) + return; + + if (event && state.activePointerId !== event.pointerId) + return; + + state.isDragging = false; + state.activePointerId = null; + state.root.classList.remove('bb-split-view-dragging'); + + window.blazorBootstrap.splitView.emitResizeEvent(state, 'OnResizeEndedJS'); + }, + update: (elementId, orientation, primaryPaneSize, minimumPaneSize, isDisabled) => { + let state = window.blazorBootstrap.splitView.instances[elementId]; + if (!state) + return; + + window.blazorBootstrap.splitView.updateState(state, orientation, primaryPaneSize, minimumPaneSize, isDisabled); + }, + updateState: (state, orientation, primaryPaneSize, minimumPaneSize, isDisabled) => { + state.orientation = orientation; + state.minimumPaneSize = minimumPaneSize; + state.isDisabled = isDisabled; + + if (state.isDragging && isDisabled) + window.blazorBootstrap.splitView.stopDragging(state); + + window.blazorBootstrap.splitView.applyPaneSize(state, primaryPaneSize); + } + }, tabs: { initialize: (elementId, dotNetHelper) => { let navTabsEl = document.getElementById(elementId); diff --git a/docs/docs/05-components/split-view.mdx b/docs/docs/05-components/split-view.mdx new file mode 100644 index 000000000..9bbb2b790 --- /dev/null +++ b/docs/docs/05-components/split-view.mdx @@ -0,0 +1,225 @@ +--- +title: Blazor Split View Component +description: The Blazor Bootstrap Split View component arranges two resizable panes horizontally or vertically with live drag updates. + +sidebar_label: Split View +sidebar_position: 26 +--- + +import CarbonAd from '/carbon-ad.mdx' + +# Blazor Split View + +The Blazor Bootstrap **Split View** component arranges two panes with a draggable divider so users can resize the layout interactively. It supports horizontal and vertical orientations, percentage-based sizing, nested layouts, live resize events, and Bootstrap-aligned divider theming. + + + +## Parameters + +| Name | Type | Default | Required | Description | Added Version | +|:--|:--|:--|:--|:--|:--| +| Color | `SplitViewColor` | `SplitViewColor.None` | | Gets or sets the divider color. | 4.0.0 | +| CustomColor | `string?` | null | | Gets or sets a custom divider color. Accepts any valid CSS color expression, including CSS variables. | 4.0.0 | +| IsDisabled | `bool` | `false` | | Gets or sets a value indicating whether user interaction is disabled. | 4.0.0 | +| MinimumPaneSize | `double` | `0` | | Gets or sets the minimum pane size as a percentage. | 4.0.0 | +| Orientation | `SplitViewOrientation` | `SplitViewOrientation.Horizontal` | | Gets or sets the SplitView orientation. | 4.0.0 | +| Pane1 | `RenderFragment` | null | ✔️ | Gets or sets the first pane content. | 4.0.0 | +| Pane2 | `RenderFragment` | null | ✔️ | Gets or sets the second pane content. | 4.0.0 | +| PrimaryPaneSize | `double` | `50` | | Gets or sets the primary pane size as a percentage. | 4.0.0 | + +## Callback Events + +| Event | Description | Added Version | +|--|--|--| +| OnResizeEnded | Fired when resizing ends. | 4.0.0 | +| OnResized | Fired while the divider is being dragged. | 4.0.0 | +| OnResizeStarted | Fired when resizing starts. | 4.0.0 | +| PrimaryPaneSizeChanged | Fired when the primary pane size changes. | 4.0.0 | + +## Enums + +### SplitViewColor + +| Name | Description | +|--|--| +| None | Uses the shared SplitView CSS variable defaults. | +| Primary | Uses the Bootstrap primary color token. | +| Secondary | Uses the Bootstrap secondary color token. | +| Success | Uses the Bootstrap success color token. | +| Danger | Uses the Bootstrap danger color token. | +| Warning | Uses the Bootstrap warning color token. | +| Info | Uses the Bootstrap info color token. | +| Light | Uses the Bootstrap light color token. | +| Dark | Uses the Bootstrap dark color token. | +| Body | Uses the Bootstrap body background token. | +| BodySecondary | Uses the Bootstrap secondary body background token. | +| BodyTertiary | Uses the Bootstrap tertiary body background token. | +| Black | Uses the Bootstrap black token. | +| White | Uses the Bootstrap white token. | +| Transparent | Uses a transparent divider color. | + +### SplitViewOrientation + +| Name | Description | +|--|--| +| Horizontal | Arranges panes side by side with a vertical divider. | +| Vertical | Arranges panes top to bottom with a horizontal divider. | + +## Examples + +### Pane content + +```cshtml {} showLineNumbers + + +
Pane1 content
+
+ +
Pane2 content
+
+
+``` + +[See demo here.](https://demos.blazorbootstrap.com/split-view#pane-content) + +### Orientation + +```cshtml {} showLineNumbers + + +
Summary
+
+ +
Details
+
+
+``` + +[See demo here.](https://demos.blazorbootstrap.com/split-view#orientation) + +### PrimaryPaneSizeChanged + +```cshtml {} showLineNumbers + + +
Primary pane
+
+ +
Secondary pane
+
+
+``` + +```cs {} showLineNumbers +@code { + private double primaryPaneSize = 50; +} +``` + +[See demo here.](https://demos.blazorbootstrap.com/split-view#primary-pane-size-changed) + +### Semantic Color + +```cshtml {} showLineNumbers + + +
Filters
+
+ +
Results
+
+
+``` + +Hover and dragging states are derived automatically in shared CSS from the resolved base divider color. + +[See demo here.](https://demos.blazorbootstrap.com/split-view#semantic-color) + +### CustomColor + +```cshtml {} showLineNumbers + + +
Pane1
+
+ +
Pane2
+
+
+``` + +[See demo here.](https://demos.blazorbootstrap.com/split-view#custom-color) + +### Resize events + +```cshtml {} showLineNumbers + + +
Primary pane
+
+ +
Secondary pane
+
+
+``` + +```cs {} showLineNumbers +@code { + private void OnResizeStarted(SplitViewResizeEventArgs args) + { + } + + private void OnResized(SplitViewResizeEventArgs args) + { + } + + private void OnResizeEnded(SplitViewResizeEventArgs args) + { + } +} +``` + +[See demo here.](https://demos.blazorbootstrap.com/split-view#onresizestarted) +[See demo here.](https://demos.blazorbootstrap.com/split-view#onresized) +[See demo here.](https://demos.blazorbootstrap.com/split-view#onresizeended) + +### Nested layouts + +`SplitView` supports nested layouts, which is useful for editors, dashboards, and multi-pane administration tools. + +```cshtml {} showLineNumbers + + +
Explorer
+
+ + + +
Editor
+
+ +
Preview
+
+
+
+
+``` + +[See demo here.](https://demos.blazorbootstrap.com/split-view#nested-layouts) + +### App shell layout + +You can combine multiple SplitView instances to build an app shell with navigation, content, and inspector panes. + +[See demo here.](https://demos.blazorbootstrap.com/split-view#app-shell) \ No newline at end of file