Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
.mul-select-item {
display: flex;
flex: 1;
align-items: center;
margin: 0 0.5rem;
}

.mul-select-item span {
flex: 1;
margin-inline-start: 0.5rem;
}
4 changes: 2 additions & 2 deletions src/BootstrapBlazor/BootstrapBlazor.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<Version>9.2.8-beta05</Version>
<Version>9.2.8-beta06</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
14 changes: 6 additions & 8 deletions src/BootstrapBlazor/Components/Select/MultiSelect.razor
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{
<BootstrapLabel required="@Required" for="@Id" ShowLabelTooltip="ShowLabelTooltip" Value="@DisplayText"></BootstrapLabel>
}
<div @attributes="@AdditionalAttributes" class="@ClassString" id="@Id">
<div @attributes="@AdditionalAttributes" class="@ClassString" id="@Id" data-bb-scroll-behavior="@ScrollIntoViewBehaviorString">
<div class="@ToggleClassString" data-bs-toggle="@ToggleString" data-bs-placement="@PlacementString" data-bs-offset="@OffsetString" data-bs-auto-close="outside" data-bs-custom-class="@CustomClassString" tabindex="0">
@if(!CheckCanEdit())
{
Expand Down Expand Up @@ -49,13 +49,11 @@
}
</div>
<div class="dropdown-menu">
@if (ShowSearch)
{
<div class="search">
<input type="text" class="form-control" @bind="@SearchText" @bind:event="oninput" />
<i class="@SearchIconString"></i>
</div>
}
<div class="@SearchClassString">
<input type="text" class="form-control search-text" autocomplete="off" value="@SearchText" aria-label="search" />
<i class="@SearchIconString"></i>
<i class="@SearchLoadingIconString"></i>
</div>
@if (ShowToolbar)
{
<div class="toolbar">
Expand Down
44 changes: 43 additions & 1 deletion src/BootstrapBlazor/Components/Select/MultiSelect.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ public partial class MultiSelect<TValue>
.AddClass("d-none", SelectedItems.Count != 0)
.Build();

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

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

/// <summary>
/// 获得/设置 绑定数据集
/// </summary>
Expand Down Expand Up @@ -195,6 +206,8 @@ public partial class MultiSelect<TValue>

private string? PlaceholderString => SelectedItems.Count == 0 ? PlaceHolder : null;

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

/// <summary>
/// OnParametersSet 方法
/// </summary>
Expand Down Expand Up @@ -241,7 +254,7 @@ protected override void OnAfterRender(bool firstRender)
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, nameof(ToggleRow));
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new { ConfirmMethodCallback = nameof(ConfirmSelectedItem), SearchMethodCallback = nameof(TriggerOnSearch), ToggleRow = nameof(ToggleRow) });

/// <summary>
/// FormatValueAsString 方法
Expand All @@ -254,6 +267,22 @@ protected override void OnAfterRender(bool firstRender)

private bool _isToggle;

/// <summary>
/// 客户端回车回调方法
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
[JSInvokable]
public async Task ConfirmSelectedItem(int index)
{
var rows = GetData();
if (index < rows.Count)
{
await ToggleRow(rows[index].Value);
StateHasChanged();
}
}

/// <summary>
/// 切换当前选项方法
/// </summary>
Expand Down Expand Up @@ -508,4 +537,17 @@ private void ResetItems()
}
}
}

/// <summary>
/// 客户端搜索栏回调方法
/// </summary>
/// <param name="searchText"></param>
/// <returns></returns>
[JSInvokable]
public Task TriggerOnSearch(string searchText)
{
SearchText = searchText;
StateHasChanged();
return Task.CompletedTask;
}
}
110 changes: 99 additions & 11 deletions src/BootstrapBlazor/Components/Select/MultiSelect.razor.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,100 @@
import { isDisabled, getTransitionDelayDurationFromElement } from "../../modules/utility.js"
import { debounce, isDisabled, getTransitionDelayDurationFromElement } from "../../modules/utility.js"
import Data from "../../modules/data.js"
import Popover from "../../modules/base-popover.js"
import EventHandler from "../../modules/event-handler.js"
import Input from "../../modules/input.js"

export function init(id, invoke, method) {
export function init(id, invoke, options) {
const el = document.getElementById(id)

if (el == null) {
if (el === null) {
return
}

const { confirmMethodCallback, searchMethodCallback, toggleRow } = options;
const search = el.querySelector(".search-text");
const itemsElement = el.querySelector('.multi-select-items');
const popover = Popover.init(el, {
itemsElement,
closeButtonSelector: '.multi-select-close'
})

const ms = {
el, invoke, method,
el, invoke,
itemsElement,
closeButtonSelector: '.multi-select-close',
search,
popover
}

const onSearch = debounce(async v => {
search.parentElement.classList.add('l');
await invoke.invokeMethodAsync('TriggerOnSearch', v);
search.parentElement.classList.remove('l');
});

const shown = () => {
if (search) {
search.focus();
}
const active = popover.toggleMenu.querySelector('.dropdown-item.active');
if (active) {
scrollIntoView(el, active);
}
}

const keydown = e => {
const menu = popover.toggleMenu;
const key = e.key;
if (key === "Enter" || key === 'NumpadEnter') {
if (popover.isPopover) {
popover.hide();
}
else {
menu.classList.remove('show');
}
const activeItem = menu.querySelector('.active');
let index = indexOf(menu, 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');
}

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;
}
current = items[index];
current.classList.add('active');
scrollIntoView(el, current);
}
}
}

Input.composition(search, onSearch);
EventHandler.on(search, 'keydown', keydown);

if (popover.isPopover) {
EventHandler.on(el, 'shown.bs.popover', shown);
}
else {
EventHandler.on(el, 'shown.bs.dropdown', shown);
}

EventHandler.on(itemsElement, 'click', '.multi-select-input', e => {
const handler = setTimeout(() => {
clearTimeout(handler);
Expand Down Expand Up @@ -50,23 +122,23 @@ export function init(id, invoke, method) {
}
});

if (!ms.popover.isPopover) {
if (!popover.isPopover) {
EventHandler.on(itemsElement, 'click', ms.closeButtonSelector, () => {
const dropdown = bootstrap.Dropdown.getInstance(popover.toggleElement)
if (dropdown && dropdown._isShown()) {
dropdown.hide()
}
})
}
ms.popover.clickToggle = e => {
popover.clickToggle = e => {
const element = e.target.closest(ms.closeButtonSelector);
if (element) {
e.stopPropagation()

invoke.invokeMethodAsync(method, element.getAttribute('data-bb-val'))
invoke.invokeMethodAsync(toggleRow, element.getAttribute('data-bb-val'))
}
}
ms.popover.isDisabled = () => {
popover.isDisabled = () => {
return isDisabled(ms.popover.toggleElement)
}

Expand Down Expand Up @@ -99,8 +171,24 @@ export function dispose(id) {
const ms = Data.get(id)
Data.remove(id)

if (!ms.popover.isPopover) {
const { search, popover } = ms;
if (!popover.isPopover) {
EventHandler.off(ms.itemsElement, 'click', ms.closeButtonSelector)
}
Popover.dispose(ms.popover)
Popover.dispose(ms.popover);

if (search) {
Input.dispose(search);
EventHandler.off(search, 'keydown');
}
}

function indexOf(el, element) {
const items = el.querySelectorAll('.dropdown-item')
return Array.prototype.indexOf.call(items, element)
}

const scrollIntoView = (el, item) => {
const behavior = el.getAttribute('data-bb-scroll-behavior') ?? 'smooth';
item.scrollIntoView({ behavior: behavior, block: "nearest", inline: "start" });
}
2 changes: 1 addition & 1 deletion src/BootstrapBlazor/Components/Select/Select.razor
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
}
<div class="dropdown-menu">
<div class="@SearchClassString">
<input type="text" class="search-text form-control" autocomplete="off" value="@SearchText" aria-label="Search">
<input type="text" class="search-text form-control" autocomplete="off" value="@SearchText" aria-label="search">
<i class="@SearchIconString"></i>
<i class="@SearchLoadingIconString"></i>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/BootstrapBlazor/Components/Select/Select.razor.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import Input from "../../modules/input.js"

export function init(id, invoke, options) {
const el = document.getElementById(id)
const { confirmMethodCallback, searchMethodCallback } = options;
if (el == null) {
if (el === null) {
return
}

const { confirmMethodCallback, searchMethodCallback } = options;
const search = el.querySelector(".search-text")
const popover = Popover.init(el)

Expand Down
Loading