Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/BootstrapBlazor/BootstrapBlazor.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<Version>9.2.1</Version>
<Version>9.2.2</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
16 changes: 7 additions & 9 deletions src/BootstrapBlazor/Components/Select/Select.razor
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
{
<BootstrapLabel required="@Required" for="@InputId" ShowLabelTooltip="ShowLabelTooltip" Value="@DisplayText" />
}
<div @attributes="AdditionalAttributes" id="@Id" class="@ClassString">
<div @attributes="AdditionalAttributes" id="@Id" class="@ClassString" data-bb-scroll-behavior="@ScrollIntoViewBehaviorString">
<CascadingValue Value="this" IsFixed="true">
@Options
</CascadingValue>
Expand All @@ -31,13 +31,11 @@
<span class="@ClearClassString" @onclick="OnClearValue"><i class="@ClearIcon"></i></span>
}
<div class="dropdown-menu">
@if (ShowSearch)
{
<div class="@SearchClassString">
<input type="text" class="search-text form-control" autocomplete="off" value="@SearchText" aria-label="Search">
<i class="@SearchIconString"></i>
</div>
}
<div class="@SearchClassString">
<input type="text" class="search-text form-control" autocomplete="off" value="@SearchText" aria-label="Search">
<i class="@SearchIconString"></i>
<i class="@SearchLoadingIconString"></i>
</div>
@if (IsVirtualize)
{
<div class="dropdown-virtual">
Expand Down Expand Up @@ -93,7 +91,7 @@
}
else if (IsMarkupString)
{
@((MarkupString)item.Text)
@((MarkupString)item.Text)
}
else
{
Expand Down
12 changes: 11 additions & 1 deletion src/BootstrapBlazor/Components/Select/Select.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,21 @@ public partial class Select<TValue> : ISelect, ILookup
.Build();

private string? SearchClassString => CssBuilder.Default("search")
.AddClass("show", ShowSearch)
.AddClass("is-fixed", IsFixedSearch)
.Build();

/// <summary>
/// 获得 SearchLoadingIcon 图标字符串
/// </summary>
private string? SearchLoadingIconString => CssBuilder.Default("icon searching-icon")
.AddClass(SearchLoadingIcon)
.Build();

private readonly List<SelectedItem> _children = [];

private string? ScrollIntoViewBehaviorString => ScrollIntoViewBehavior == ScrollIntoViewBehavior.Smooth ? null : ScrollIntoViewBehavior.ToDescriptionString();

/// <summary>
/// 获得/设置 右侧清除图标 默认 fa-solid fa-angle-up
/// </summary>
Expand Down Expand Up @@ -298,7 +308,7 @@ private SelectedItem? SelectedRow
{
var item = Rows.Find(i => i.Value == CurrentValueAsString)
?? Rows.Find(i => i.Active)
?? Rows.Where(i => !i.IsDisabled).FirstOrDefault()
?? Rows.FirstOrDefault(i => !i.IsDisabled)
?? GetVirtualizeItem(CurrentValueAsString);

if (item != null)
Expand Down
103 changes: 59 additions & 44 deletions src/BootstrapBlazor/Components/Select/Select.razor.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,55 +26,51 @@ export function init(id, invoke, options) {
}

const keydown = e => {
if (popover.toggleElement.classList.contains('show')) {
const items = popover.toggleMenu.querySelectorAll('.dropdown-item:not(.search, .disabled)')
let activeItem = popover.toggleMenu.querySelector('.dropdown-item.preActive')
if (activeItem == null) activeItem = popover.toggleMenu.querySelector('.dropdown-item.active')

if (activeItem) {
if (items.length > 1) {
activeItem.classList.remove('preActive')
if (e.key === "ArrowUp") {
do {
activeItem = activeItem.previousElementSibling
}
while (activeItem && !activeItem.classList.contains('dropdown-item'))
if (!activeItem) {
activeItem = items[items.length - 1]
}
activeItem.classList.add('preActive')
scrollToActive(popover.toggleMenu, activeItem)
e.preventDefault()
e.stopPropagation()
}
else if (e.key === "ArrowDown") {
do {
activeItem = activeItem.nextElementSibling
}
while (activeItem && !activeItem.classList.contains('dropdown-item'))
if (!activeItem) {
activeItem = items[0]
}
activeItem.classList.add('preActive')
scrollToActive(popover.toggleMenu, activeItem)
e.preventDefault()
e.stopPropagation()
}
const menu = popover.toggleMenu;
const key = e.key;
if (key === "Enter" || key === 'NumpadEnter') {
menu.classList.remove('show')
let index = indexOf(el, activeItem)
invoke.invokeMethodAsync(confirmMethodCallback, index)
}
else if (key === 'ArrowUp' || key === 'ArrowDown') {
e.preventDefault();

if (menu.querySelector('.dropdown-virtual')) {
return;
}

const items = [...menu.querySelectorAll('.dropdown-item:not(.disabled)')];
if (items.length > 0) {
let current = menu.querySelector('.active');
if (current !== null) {
current.classList.remove('active');
}

if (e.key === "Enter") {
popover.toggleMenu.classList.remove('show')
let index = indexOf(el, activeItem)
invoke.invokeMethodAsync(confirmMethodCallback, index)
let index = current === null ? -1 : items.indexOf(current);
index = key === 'ArrowUp' ? index - 1 : index + 1;
if (index < 0) {
index = items.length - 1;
}
else if (index > items.length - 1) {
index = 0;
}
items[index].classList.add('active');
const top = getTop(menu, index);
const hehavior = el.getAttribute('data-bb-scroll-behavior') ?? 'smooth';
menu.scrollTo({ top: top, left: 0, behavior: hehavior });
}
}
}

EventHandler.on(el, 'shown.bs.dropdown', shown);
EventHandler.on(el, 'keydown', keydown)

const onSearch = debounce(v => invoke.invokeMethodAsync(searchMethodCallback, v));
const onSearch = debounce(async v => {
search.parentElement.classList.add('l');
await invoke.invokeMethodAsync(searchMethodCallback, v);
search.parentElement.classList.remove('l');
});
if (search) {
Input.composition(search, onSearch);
}
Expand All @@ -87,6 +83,19 @@ export function init(id, invoke, options) {
Data.set(id, select);
}

const getTop = (menu, index) => {
const styles = getComputedStyle(menu)
const maxHeight = parseInt(styles.maxHeight) / 2
const itemHeight = getHeight(menu.querySelector('.dropdown-item'))
const height = itemHeight * index
const count = Math.floor(maxHeight / itemHeight);
let top = 0;
if (height > maxHeight) {
top = itemHeight * (index > count ? index - count : index)
}
return top;
}

export function show(id) {
const select = Data.get(id)
if (select) {
Expand Down Expand Up @@ -122,20 +131,26 @@ export function dispose(id) {
}

function scrollToActive(el, activeItem) {
if (!activeItem) {
activeItem = el.querySelector('.dropdown-item.active')
}
const virtualEl = el.querySelector('.dropdown-virtual');

activeItem ??= el.querySelector('.dropdown-item.active')

if (activeItem) {
const innerHeight = getInnerHeight(el)
const itemHeight = getHeight(activeItem);
const index = indexOf(el, activeItem)
const margin = itemHeight * index - (innerHeight - itemHeight) / 2;
const hehavior = el.getAttribute('data-bb-scroll-behavior') ?? 'smooth';

const search = el.querySelector('.search');
if (search.classList.contains('show')) {

}
if (margin >= 0) {
el.scrollTo(0, margin);
el.scrollTo({ top: margin, left: 0, behavior: hehavior });
}
else {
el.scrollTo(0, 0);
el.scrollTo({ top: margin, left: 0, behavior: hehavior });
}
}
}
Expand Down
18 changes: 18 additions & 0 deletions src/BootstrapBlazor/Components/Select/Select.razor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,24 @@
position: relative;
border-block-end: var(--bs-border-width) solid var(--bb-select-search-border-color);
margin-block-end: var(--bb-select-search-margin-bottom);

&:not(.show) {
display: none;
}

&.l {
.search-icon {
display: none;
}

.searching-icon {
display: block;
}
}

.searching-icon {
display: none;
}
}

.dropdown-menu .search.is-fixed {
Expand Down
17 changes: 15 additions & 2 deletions src/BootstrapBlazor/Components/Select/SelectBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public abstract class SelectBase<TValue> : PopoverSelectBase<TValue>
[Parameter]
public string? SearchIcon { get; set; }

/// <summary>
/// 获得/设置 设置正在搜索图标
/// </summary>
[Parameter]
public string? SearchLoadingIcon { get; set; }

/// <summary>
/// 获得/设置 右侧下拉箭头图标 默认 fa-solid fa-angle-up
/// </summary>
Expand All @@ -42,7 +48,7 @@ public abstract class SelectBase<TValue> : PopoverSelectBase<TValue>
public bool IsMarkupString { get; set; }

/// <summary>
/// 获得/设置 字符串比较规则 默认 StringComparison.OrdinalIgnoreCase 大小写不敏感
/// 获得/设置 字符串比较规则 默认 StringComparison.OrdinalIgnoreCase 大小写不敏感
/// </summary>
[Parameter]
public StringComparison StringComparison { get; set; } = StringComparison.OrdinalIgnoreCase;
Expand All @@ -53,6 +59,12 @@ public abstract class SelectBase<TValue> : PopoverSelectBase<TValue>
[Parameter]
public RenderFragment<string>? GroupItemTemplate { get; set; }

/// <summary>
/// 获得/设置 滚动行为 默认 <see cref="ScrollIntoViewBehavior.Smooth"/>
/// </summary>
[Parameter]
public ScrollIntoViewBehavior ScrollIntoViewBehavior { get; set; } = ScrollIntoViewBehavior.Smooth;

/// <summary>
/// 获得/设置 IIconTheme 服务实例
/// </summary>
Expand All @@ -69,7 +81,7 @@ public abstract class SelectBase<TValue> : PopoverSelectBase<TValue>
/// <summary>
/// 获得 SearchIcon 图标字符串 默认增加 icon 样式
/// </summary>
protected string? SearchIconString => CssBuilder.Default("icon")
protected string? SearchIconString => CssBuilder.Default("icon search-icon")
.AddClass(SearchIcon)
.Build();

Expand Down Expand Up @@ -98,6 +110,7 @@ protected override void OnParametersSet()
base.OnParametersSet();

SearchIcon ??= IconTheme.GetIconByKey(ComponentIcons.SelectSearchIcon);
SearchLoadingIcon ??= IconTheme.GetIconByKey(ComponentIcons.SearchButtonLoadingIcon);
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions src/BootstrapBlazor/wwwroot/scss/theme/bootstrapblazor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -511,8 +511,8 @@ $bb-select-search-margin-bottom: .5rem;
$bb-select-search-border-color: var(--bs-border-color);
$bb-select-search-padding-right: 30px;
$bb-select-search-icon-color: var(--bb-select-search-border-color);
$bb-select-search-icon-right: 18px;
$bb-select-search-icon-top: 18px;
$bb-select-search-icon-right: 20px;
$bb-select-search-icon-top: 15px;
$bb-select-search-height: 60px;
$bb-select-append-width: 30px;
$bb-select-append-color: #c0c4cc;
Expand Down
30 changes: 29 additions & 1 deletion test/UnitTest/Components/SelectTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -489,8 +489,36 @@ public void IsFixedSearch_Ok()
pb.Add(a => a.ShowSearch, true);
pb.Add(a => a.IsFixedSearch, true);
});
Assert.Contains("search show is-fixed", cut.Markup);
Assert.Contains("class=\"icon search-icon", cut.Markup);

cut.SetParametersAndRender(pb =>
{
pb.Add(a => a.ShowSearch, false);
});
Assert.Contains("search is-fixed", cut.Markup);
Assert.Contains("class=\"icon", cut.Markup);
}

[Fact]
public void ScrollIntoViewBehavior_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
{
new("1", "Test1"),
new("2", "Test2")
});
pb.Add(a => a.Value, "2");
pb.Add(a => a.ScrollIntoViewBehavior, ScrollIntoViewBehavior.Auto);
});
Assert.Contains("data-bb-scroll-behavior=\"auto\"", cut.Markup);

cut.SetParametersAndRender(pb =>
{
pb.Add(a => a.ScrollIntoViewBehavior, ScrollIntoViewBehavior.Smooth);
});
Assert.DoesNotContain("data-bb-scroll-behavior", cut.Markup);
}

[Fact]
Expand Down
Loading