-
- @FirstPaneTemplate
-
-
-
- @SecondPaneTemplate
+
+ @FirstPaneTemplate
+
+
+ @SecondPaneTemplate
+
+
+ @if (IsCollapsible)
+ {
+
+ }
+
+ @if (IsCollapsible)
+ {
+
+ }
diff --git a/src/BootstrapBlazor/Components/Split/Split.razor.cs b/src/BootstrapBlazor/Components/Split/Split.razor.cs
index 0b5b06c50ee..1c31dce9201 100644
--- a/src/BootstrapBlazor/Components/Split/Split.razor.cs
+++ b/src/BootstrapBlazor/Components/Split/Split.razor.cs
@@ -5,7 +5,7 @@
namespace BootstrapBlazor.Components;
///
-///
+/// Split 组件
///
public sealed partial class Split
{
@@ -13,16 +13,10 @@ public sealed partial class Split
/// 获得 组件样式
///
private string? ClassString => CssBuilder.Default("split")
+ .AddClass("is-vertical", IsVertical)
.AddClassFromAttributes(AdditionalAttributes)
.Build();
- ///
- /// 获得 组件 Wrapper 样式
- ///
- private string? WrapperClassString => CssBuilder.Default("split-wrapper")
- .AddClass("is-horizontal", !IsVertical)
- .Build();
-
///
/// 获得 第一个窗格 Style
///
@@ -30,6 +24,12 @@ public sealed partial class Split
.AddClass($"flex-basis: {Basis.ConvertToPercentString()};")
.Build();
+ ///
+ /// 获取 是否开启折叠功能 默认 false
+ ///
+ [Parameter]
+ public bool IsCollapsible { get; set; }
+
///
/// 获得/设置 是否垂直分割
///
@@ -53,4 +53,30 @@ public sealed partial class Split
///
[Parameter]
public RenderFragment? SecondPaneTemplate { get; set; }
+
+ ///
+ /// 获得/设置 窗格折叠时回调方法 参数 bool 值为 true 是表示已折叠 值为 false 表示第二个已折叠
+ ///
+ [Parameter]
+ public Func
? OnCollapsedAsync { get; set; }
+
+ ///
+ ///
+ ///
+ ///
+ protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, nameof(TriggerOnCollapsed));
+
+ ///
+ /// 窗格折叠时回调方法 由 JavaScript 调用
+ ///
+ ///
+ ///
+ [JSInvokable]
+ public async Task TriggerOnCollapsed(bool collapsed)
+ {
+ if (OnCollapsedAsync != null)
+ {
+ await OnCollapsedAsync(collapsed);
+ }
+ }
}
diff --git a/src/BootstrapBlazor/Components/Split/Split.razor.js b/src/BootstrapBlazor/Components/Split/Split.razor.js
index 392aed0bfa7..227337d3bbd 100644
--- a/src/BootstrapBlazor/Components/Split/Split.razor.js
+++ b/src/BootstrapBlazor/Components/Split/Split.razor.js
@@ -1,9 +1,8 @@
-import { getHeight, getInnerHeight } from "../../modules/utility.js"
-import Data from "../../modules/data.js"
+import Data from "../../modules/data.js"
import Drag from "../../modules/drag.js"
import EventHandler from "../../modules/event-handler.js"
-export function init(id) {
+export function init(id, invoke, method) {
const el = document.getElementById(id)
if (el === null) {
return
@@ -17,11 +16,10 @@ export function init(id) {
let newVal = 0
let originX = 0
let originY = 0
- const splitWrapper = el.firstElementChild
- const isVertical = !splitWrapper.classList.contains('is-horizontal')
- const splitBar = splitWrapper.children[1]
- const splitLeft = splitWrapper.children[0]
- const splitRight = splitWrapper.children[2];
+ const isVertical = el.classList.contains('is-vertical')
+ const splitLeft = el.children[0];
+ const splitRight = el.children[1];
+ const splitBar = el.children[2];
split.splitBar = splitBar;
Drag.drag(splitBar,
@@ -58,7 +56,35 @@ export function init(id) {
() => {
el.classList.remove('dragging');
removeMask(splitLeft, splitRight);
- })
+ }
+ );
+
+ let start = 0;
+ const step = ts => {
+ if (start === 0) {
+ start = ts;
+ }
+ if (ts - start > 300) {
+ splitLeft.classList.remove('is-collapsed');
+ }
+ requestAnimationFrame(step);
+ }
+
+ EventHandler.on(splitBar, 'click', '.split-bar-arrow', e => {
+ var element = e.delegateTarget;
+ splitLeft.classList.add('is-collapsed');
+ if (element.classList.contains("split-bar-arrow-left")) {
+ splitLeft.style.setProperty('flex-basis', '0%');
+ invoke.invokeMethodAsync(method, true);
+ }
+ else {
+ splitLeft.style.setProperty('flex-basis', '100%');
+ invoke.invokeMethodAsync(method, false);
+ }
+ splitRight.style.removeProperty('flex-basis');
+ start = 0;
+ requestAnimationFrame(step);
+ });
}
const showMask = (left, right) => {
@@ -89,6 +115,9 @@ export function dispose(id) {
if (split) {
const { el } = split;
- Drag.dispose(el)
+ if (el.splitBar) {
+ EventHandler.off(splitBar, 'click', '.split-bar-arrow');
+ Drag.dispose(el.splitBar);
+ }
}
}
diff --git a/src/BootstrapBlazor/Components/Split/Split.razor.scss b/src/BootstrapBlazor/Components/Split/Split.razor.scss
index 4c477069dff..c3242fe4919 100644
--- a/src/BootstrapBlazor/Components/Split/Split.razor.scss
+++ b/src/BootstrapBlazor/Components/Split/Split.razor.scss
@@ -1,83 +1,297 @@
+[data-bs-theme='dark'] .split {
+ --bb-split-bar-bg: #{$bb-split-bar-bg-dark};
+ --bb-split-bar-handle-bg: #{$bb-split-bar-handle-bg-dark};
+ --bb-split-bar-handle-color: #{$bb-split-bar-handle-color-dark};
+ --bb-split-bar-handle-hover-bg: #{$bb-split-bar-handle-hover-bg-dark};
+ --bb-split-bar-handle-hover-color: #{$bb-split-bar-handle-hover-color-dark};
+ --bb-split-bar-arrow-hover-border-color: #{$bb-split-bar-arrow-hover-border-color-dark};
+}
+
.split {
- --bb-split-bar-height: #{$bb-split-bar-height};
- --bb-split-bar-hover-bg: #{$bb-split-bar-hover-bg};
- display: block;
+ --bb-split-bar-bg: #{$bb-split-bar-bg};
+ --bb-split-bar-hover-bg: var(--bb-primary-color);
+ --bb-split-bar-width: #{$bb-split-bar-width};
+ --bb-split-bar-handle-bg: #{$bb-split-bar-handle-bg};
+ --bb-split-bar-handle-color: #{$bb-split-bar-handle-color};
+ --bb-split-bar-handle-hover-bg: #{$bb-split-bar-handle-hover-bg};
+ --bb-split-bar-handle-hover-color: #{$bb-split-bar-handle-hover-color};
+ --bb-split-bar-arrow-bg: #{$bb-split-bar-arrow-bg};
+ --bb-split-bar-arrow-border-color: #{$bb-split-bar-arrow-border-color};
+ --bb-split-bar-arrow-hover-bg: #{$bb-split-bar-arrow-hover-bg};
+ --bb-split-bar-arrow-hover-border-color: #{$bb-split-bar-arrow-hover-border-color};
height: 100%;
width: 100%;
+ display: flex;
+ flex-direction: row;
+ overflow: hidden;
- .split-wrapper {
- display: flex;
+ &.is-vertical {
flex-direction: column;
- height: 100%;
- }
- .split-wrapper.is-horizontal {
- flex-direction: row;
- }
+ .split-bar {
+ cursor: row-resize;
+ width: auto;
+ height: var(--bb-split-bar-width);
- &.dragging .split-wrapper {
- cursor: row-resize;
- }
+ &:after {
+ content: "";
+ cursor: row-resize;
+ display: block;
+ height: 16px;
+ position: absolute;
+ width: 100%;
+ z-index: 12;
+ }
- &.dragging .split-wrapper.is-horizontal {
- cursor: col-resize;
+ &:hover {
+ .split-bar-arrow-left {
+ &:before {
+ transform: rotate(125deg);
+ }
+
+ &:after {
+ transform: rotate(50deg);
+ }
+ }
+
+ .split-bar-arrow-right {
+ &:before {
+ transform: rotate(55deg);
+ transform-origin: 1px 6px 0;
+ }
+
+ &:after {
+ transform: rotate(125deg);
+ transform-origin: 1px 2px 0;
+ }
+ }
+ }
+
+ .split-bar-arrow {
+ width: 26px;
+
+ &:before, &:after {
+ content: "";
+ border-radius: 16px;
+ height: 8px;
+ transform: rotate(90deg);
+ width: 2px;
+ }
+ }
+
+ .split-bar-arrow-left {
+ left: 24px;
+ bottom: 20px;
+
+ &:before {
+ top: 3px;
+ left: 10px;
+ transform-origin: 2px 7px 0;
+ }
+
+ &:after {
+ left: 11px;
+ top: 8px;
+ transform-origin: 1px 1px 0;
+ }
+ }
+
+ .split-bar-arrow-right {
+ left: -25px;
+ top: 20px;
+
+ &:before {
+ left: 12px;
+ top: 8px;
+ transform-origin: 1px 6px 0;
+ }
+
+ &:after {
+ left: 10px;
+ top: 12px;
+ transform-origin: 1px 2px 0;
+ }
+ }
+
+ .split-bar-handler {
+ width: 24px;
+ padding: 0 2px;
+ flex-direction: column;
+
+ > div {
+ width: 100%;
+ height: 2px;
+
+ &:not(:first-child) {
+ margin-left: 0;
+ margin-top: 4px;
+ }
+ }
+ }
+ }
}
.split-pane {
+ width: 100%;
height: 100%;
- overflow: hidden;
position: relative;
}
+ .split-left {
+ order: 0;
+ overflow: hidden;
+
+ &.is-collapsed {
+ transition: flex-basis .3s linear;
+ }
+ }
+
+ .split-right {
+ order: 2;
+ flex: 1;
+ overflow: hidden;
+ }
+
.split-bar {
+ order: 1;
+ width: var(--bb-split-bar-width);
display: flex;
justify-content: center;
align-items: center;
- height: var(--bb-split-bar-height);
- width: 100%;
- border: 1px solid var(--bs-border-color);
- border-left: none;
- border-right: none;
- cursor: row-resize;
- overflow: unset;
- }
+ cursor: col-resize;
+ background-color: var(--bb-split-bar-bg);
+ z-index: 10;
+ transition: background-color .3s linear;
- .split-bar:hover,
- &.dragging > .split-wrapper > .split-bar {
- background-color: var(--bb-split-bar-hover-bg);
- }
+ &:after {
+ content: "";
+ cursor: col-resize;
+ display: block;
+ height: 100%;
+ position: absolute;
+ width: 16px;
+ z-index: 10;
+ }
- .split-bar:hover .split-trigger-bar,
- &.dragging > .split-wrapper > .split-bar .split-trigger-bar {
- background-color: var(--bs-body-bg);
- }
+ &:hover {
+ background: var(--bb-split-bar-hover-bg);
- .split-trigger {
- font-size: 0;
- display: inline-block;
- }
+ .split-bar-handler {
+ background-color: var(--bb-split-bar-handle-hover-bg);
- .split-bar .split-trigger-bar {
- width: 1px;
- height: 4px;
- background: rgba(var(--bs-body-color-rgb), .25);
- margin-left: 3px;
- display: inline-block;
- }
+ > div {
+ background: var(--bb-split-bar-handle-hover-color);
+ }
+ }
- .split-wrapper.is-horizontal > .split-bar {
- width: 6px;
- height: 100%;
- border: 1px solid var(--bs-border-color);
- border-top: none;
- border-bottom: none;
- cursor: col-resize;
+ .split-bar-arrow {
+ background-color: var(--bb-split-bar-arrow-hover-bg);
+ border-color: var(--bb-split-bar-arrow-hover-border-color);
+ border-width: 1px;
+
+ &:before,
+ &:after {
+ background-color: var(--bb-split-bar-arrow-hover-border-color);
+ opacity: 1;
+ }
+ }
+
+ .split-bar-arrow-left {
+ &:before {
+ transform: rotate(45deg);
+ }
+
+ &:after {
+ transform: rotate(-45deg);
+ }
+ }
+
+ .split-bar-arrow-right {
+ &:before {
+ transform: rotate(-40deg);
+ }
+
+ &:after {
+ transform: rotate(40deg);
+ }
+ }
+ }
+
+ .split-bar-arrow {
+ background-color: var(--bb-split-bar-arrow-bg);
+ border-color: var(--bb-split-bar-arrow-border-color);
+ transition: background-color 500ms ease-out;
+ border-radius: 50%;
+ border-image: none;
+ border-width: 1px;
+ cursor: pointer;
+ display: flex;
+ height: 26px;
+ padding: 1px 12px;
+ position: relative;
+ opacity: 0.9;
+ z-index: 5;
+
+ &:before,
+ &:after {
+ content: "";
+ background-color: var(--bb-split-bar-arrow-bg);
+ opacity: 0;
+ transition-duration: .3s;
+ transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
+ border-radius: 16px;
+ height: 8px;
+ position: absolute;
+ transform: rotate(0deg);
+ width: 2px;
+ }
+
+ &:before {
+ top: 5px;
+ transform-origin: 1px 7px 0;
+ }
+
+ &:after {
+ top: 11px;
+ transform-origin: 1px 1px 0;
+ }
+ }
+
+ .split-bar-handler {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 24px;
+ position: relative;
+ background-color: var(--bb-split-bar-handle-bg);
+ padding: 2px 0px;
+
+ > div {
+ border-radius: 3px;
+ background: var(--bb-split-bar-handle-color);
+ width: 2px;
+ height: 100%;
+
+ &:not(:first-child) {
+ margin-left: 4px;
+ }
+ }
+ }
+
+ .split-bar-arrow-left {
+ right: 5px;
+
+ &:before, &:after {
+ left: 8px;
+ }
+ }
+
+ .split-bar-arrow-right {
+ left: 5px;
- .split-trigger-bar {
- width: 4px;
- height: 1px;
- margin-left: 0;
- margin-top: 3px;
+ &:before, &:after {
+ right: 8px;
+ }
}
}
}
diff --git a/src/BootstrapBlazor/wwwroot/scss/bootstrapblazor-dark.scss b/src/BootstrapBlazor/wwwroot/scss/bootstrapblazor-dark.scss
index 78b1e8b24ef..b409602f460 100644
--- a/src/BootstrapBlazor/wwwroot/scss/bootstrapblazor-dark.scss
+++ b/src/BootstrapBlazor/wwwroot/scss/bootstrapblazor-dark.scss
@@ -1,2 +1,10 @@
// Dial-Button
$bb-dial-item-hover-bg-dark: #313131;
+
+// Split
+$bb-split-bar-bg-dark: #495057;
+$bb-split-bar-handle-bg-dark: var(--bs-body-bg);
+$bb-split-bar-handle-color-dark: #999;
+$bb-split-bar-handle-hover-bg-dark: var(--bs-body-bg);
+$bb-split-bar-handle-hover-color-dark: #999;
+$bb-split-bar-arrow-hover-border-color-dark: #686a6c;
diff --git a/src/BootstrapBlazor/wwwroot/scss/theme/bootstrapblazor.scss b/src/BootstrapBlazor/wwwroot/scss/theme/bootstrapblazor.scss
index c587267a3a4..b375377a447 100644
--- a/src/BootstrapBlazor/wwwroot/scss/theme/bootstrapblazor.scss
+++ b/src/BootstrapBlazor/wwwroot/scss/theme/bootstrapblazor.scss
@@ -542,8 +542,16 @@ $bb-spinner-border-width-xxl: 6rem;
$bb-spinner-border-border-width-xxl: .275em;
// Split
-$bb-split-bar-height: 6px;
-$bb-split-bar-hover-bg: #409eff;
+$bb-split-bar-bg: #dee2e6;
+$bb-split-bar-width: 3px;
+$bb-split-bar-handle-bg: #fff;
+$bb-split-bar-handle-color: #606266;
+$bb-split-bar-handle-hover-bg: #fff;
+$bb-split-bar-handle-hover-color: #606266;
+$bb-split-bar-arrow-bg: rgba(0, 0, 0, 0);
+$bb-split-bar-arrow-border-color: rgba(0, 0, 0, 0);
+$bb-split-bar-arrow-hover-bg: rgba(0, 0, 0, 0);
+$bb-split-bar-arrow-hover-border-color: #0d6efd;
// Step
$bb-step-border-width: 2px;
diff --git a/test/UnitTest/Components/SplitTest.cs b/test/UnitTest/Components/SplitTest.cs
index 5db39b1dc6b..103df2438fd 100644
--- a/test/UnitTest/Components/SplitTest.cs
+++ b/test/UnitTest/Components/SplitTest.cs
@@ -11,13 +11,13 @@ public void SplitStyle_Ok()
{
var cut = Context.RenderComponent(pb =>
{
- pb.Add(b => b.FirstPaneTemplate, BuildeComponent("I am Pane1"));
- pb.Add(b => b.SecondPaneTemplate, BuildeComponent("I am Pane2"));
+ pb.Add(b => b.FirstPaneTemplate, RenderSplitView("I am Pane1"));
+ pb.Add(b => b.SecondPaneTemplate, RenderSplitView("I am Pane2"));
pb.Add(b => b.IsVertical, true);
});
Assert.Contains("I am Pane1", cut.Markup);
Assert.Contains("I am Pane2", cut.Markup);
- Assert.DoesNotContain("is-horizontal", cut.Markup);
+ Assert.Contains("is-vertical", cut.Markup);
cut.SetParametersAndRender(pb =>
{
@@ -27,15 +27,55 @@ public void SplitStyle_Ok()
cut.SetParametersAndRender(pb =>
{
- pb.Add(b => b.AdditionalAttributes, new Dictionary() { ["tag"] = "tagok" });
+ pb.Add(b => b.AdditionalAttributes, new Dictionary() { ["tag"] = "tag" });
});
- Assert.Contains("tagok", cut.Markup);
+ Assert.Contains("tag", cut.Markup);
+ }
+
+ [Fact]
+ public void IsCollapsible_Ok()
+ {
+ var cut = Context.RenderComponent(pb =>
+ {
+ pb.Add(b => b.FirstPaneTemplate, RenderSplitView("I am Pane1"));
+ pb.Add(b => b.SecondPaneTemplate, RenderSplitView("I am Pane2"));
+ });
+ cut.DoesNotContain("split-bar-arrow-left");
+ cut.DoesNotContain("split-bar-arrow-right");
+
+ cut.SetParametersAndRender(pb =>
+ {
+ pb.Add(b => b.IsCollapsible, true);
+ });
+ cut.Contains("split-bar-arrow-left");
+ cut.Contains("split-bar-arrow-right");
+ }
- RenderFragment BuildeComponent(string name = "I am Pane1") => builder =>
+ [Fact]
+ public async Task OnCollapsedAsync_Ok()
+ {
+ var state = false;
+ var cut = Context.RenderComponent(pb =>
{
- builder.OpenElement(1, "div");
- builder.AddContent(2, name);
- builder.CloseElement();
- };
+ pb.Add(b => b.FirstPaneTemplate, RenderSplitView("I am Pane1"));
+ pb.Add(b => b.SecondPaneTemplate, RenderSplitView("I am Pane2"));
+ pb.Add(b => b.IsCollapsible, true);
+ pb.Add(b => b.OnCollapsedAsync, async (collapsed) =>
+ {
+ state = collapsed;
+ await Task.CompletedTask;
+ });
+ });
+ await cut.InvokeAsync(() => cut.Instance.TriggerOnCollapsed(true));
+ Assert.True(state);
+ await cut.InvokeAsync(() => cut.Instance.TriggerOnCollapsed(false));
+ Assert.False(state);
}
+
+ static RenderFragment RenderSplitView(string name = "I am Pane1") => builder =>
+ {
+ builder.OpenElement(1, "div");
+ builder.AddContent(2, name);
+ builder.CloseElement();
+ };
}