diff --git a/BlazorBootstrap.Demo.RCL/BlazorBootstrap.Demo.RCL.csproj b/BlazorBootstrap.Demo.RCL/BlazorBootstrap.Demo.RCL.csproj index 75ddd67a4..5b4e5e876 100644 --- a/BlazorBootstrap.Demo.RCL/BlazorBootstrap.Demo.RCL.csproj +++ b/BlazorBootstrap.Demo.RCL/BlazorBootstrap.Demo.RCL.csproj @@ -20,6 +20,7 @@ + diff --git a/BlazorBootstrap.Demo.RCL/Pages/Index.razor b/BlazorBootstrap.Demo.RCL/Pages/Index.razor index 549b61833..cd9649779 100644 --- a/BlazorBootstrap.Demo.RCL/Pages/Index.razor +++ b/BlazorBootstrap.Demo.RCL/Pages/Index.razor @@ -170,6 +170,11 @@

Sidebar

+
+ +

Spinner New

+
+

Switch

diff --git a/BlazorBootstrap.Demo.RCL/Pages/Spinner/SpinnerDocumentation.razor b/BlazorBootstrap.Demo.RCL/Pages/Spinner/SpinnerDocumentation.razor new file mode 100644 index 000000000..cdf7cdb15 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Pages/Spinner/SpinnerDocumentation.razor @@ -0,0 +1,64 @@ +@page "/spinner" + +@title + + + +

Blazor Spinner

+
+ Visualize the loading state of a component or page using the Blazor Bootstrap Spinner component. +
+ +@* *@ + + +
Use the border spinners for a lightweight loading indicator.
+ + + +
+ The border spinner's border color inherits the element's color (currentColor). This means you can easily customize the spinner's color by changing the Color parameter on the standard spinner. +
+ + + +
+ If you don't fancy a border spinner, switch to the grow spinner, while it doesn't technically spin, it does repeatedly grow! +
+ + + + +
The loading dots are a special indicator for a lightweight loading indicator.
+ + + + + + + + + + + + + + + + + + + + + + + + + + +@code{ + private string pageUrl = "/spinner"; + private string title = "Blazor Spinner Component"; + private string description = "Visualize the loading state of a component or page using the Blazor Bootstrap Spinner component."; + private string imageUrl = "https://i.imgur.com/273TamX.png"; // TODO: update this +} diff --git a/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_01_Border_Spinner.razor b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_01_Border_Spinner.razor new file mode 100644 index 000000000..19883cb3f --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_01_Border_Spinner.razor @@ -0,0 +1 @@ + diff --git a/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_02_Colors.razor b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_02_Colors.razor new file mode 100644 index 000000000..d8269f61a --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_02_Colors.razor @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_03_Grow_spinner_A.razor b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_03_Grow_spinner_A.razor new file mode 100644 index 000000000..b937167d8 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_03_Grow_spinner_A.razor @@ -0,0 +1 @@ + diff --git a/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_03_Grow_spinner_B.razor b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_03_Grow_spinner_B.razor new file mode 100644 index 000000000..c11ad48de --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_03_Grow_spinner_B.razor @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_04_Loading_dots_spinner_A.razor b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_04_Loading_dots_spinner_A.razor new file mode 100644 index 000000000..13eb71bbf --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_04_Loading_dots_spinner_A.razor @@ -0,0 +1 @@ + diff --git a/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_04_Loading_dots_spinner_B.razor b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_04_Loading_dots_spinner_B.razor new file mode 100644 index 000000000..38bed6132 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_04_Loading_dots_spinner_B.razor @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_05_Alignment_A_Margin.razor b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_05_Alignment_A_Margin.razor new file mode 100644 index 000000000..610f53f16 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_05_Alignment_A_Margin.razor @@ -0,0 +1 @@ + diff --git a/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_05_Alignment_B_Palcement_Flex_01.razor b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_05_Alignment_B_Palcement_Flex_01.razor new file mode 100644 index 000000000..a1bc75cfe --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_05_Alignment_B_Palcement_Flex_01.razor @@ -0,0 +1,3 @@ +
+ +
diff --git a/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_05_Alignment_B_Palcement_Flex_02.razor b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_05_Alignment_B_Palcement_Flex_02.razor new file mode 100644 index 000000000..dabeecfb8 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_05_Alignment_B_Palcement_Flex_02.razor @@ -0,0 +1,4 @@ +
+ Loading... + +
diff --git a/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_05_Alignment_C_Palcement_Floats.razor b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_05_Alignment_C_Palcement_Floats.razor new file mode 100644 index 000000000..cfac61c99 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_05_Alignment_C_Palcement_Floats.razor @@ -0,0 +1,3 @@ +
+ +
diff --git a/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_05_Alignment_D_Palcement_Text_align.razor b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_05_Alignment_D_Palcement_Text_align.razor new file mode 100644 index 000000000..a26a69e1b --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_05_Alignment_D_Palcement_Text_align.razor @@ -0,0 +1,3 @@ +
+ +
diff --git a/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_06_Size_A_Border.razor b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_06_Size_A_Border.razor new file mode 100644 index 000000000..6f43beb0f --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_06_Size_A_Border.razor @@ -0,0 +1,4 @@ + + + + diff --git a/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_06_Size_B_Grow.razor b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_06_Size_B_Grow.razor new file mode 100644 index 000000000..86b6883e9 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_06_Size_B_Grow.razor @@ -0,0 +1,4 @@ + + + + diff --git a/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_06_Size_C_Dots.razor b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_06_Size_C_Dots.razor new file mode 100644 index 000000000..0d13af404 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_06_Size_C_Dots.razor @@ -0,0 +1,4 @@ + + + + diff --git a/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_07_Visible.razor b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_07_Visible.razor new file mode 100644 index 000000000..f15cc5ae0 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Pages/Spinner/Spinner_Demo_07_Visible.razor @@ -0,0 +1,14 @@ + + +
+ + +
+ +@code { + private bool visible = true; + + private void Hide() => visible = false; + + private void Show() => visible = true; +} diff --git a/BlazorBootstrap.Demo.RCL/Shared/MainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Shared/MainLayout.razor.cs index 016dd643f..63bd27569 100644 --- a/BlazorBootstrap.Demo.RCL/Shared/MainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Shared/MainLayout.razor.cs @@ -66,9 +66,10 @@ private IEnumerable GetNavItems() new (){ Id = "518", Text = "Progress", Href = "/progress", IconName = IconName.UsbC, ParentId = "5" }, new (){ Id = "519", Text = "Script Loader", Href = "/script-loader", IconName = IconName.CodeSlash, ParentId = "5" }, new (){ Id = "520", Text = "Sidebar", Href = "/sidebar", IconName = IconName.LayoutSidebar, ParentId = "5" }, - new (){ Id = "521", Text = "Tabs", Href = "/tabs", IconName = IconName.WindowPlus, ParentId = "5" }, - new (){ Id = "522", Text = "Toasts", Href = "/toasts", IconName = IconName.ExclamationTriangleFill, ParentId = "5" }, - new (){ Id = "523", Text = "Tooltips", Href = "/tooltips", IconName = IconName.ChatSquareDotsFill, ParentId = "5" }, + new (){ Id = "521", Text = "Spinner", Href = "/spinner", IconName = IconName.ArrowRepeat, ParentId = "5" }, + new (){ Id = "522", Text = "Tabs", Href = "/tabs", IconName = IconName.WindowPlus, ParentId = "5" }, + new (){ Id = "523", Text = "Toasts", Href = "/toasts", IconName = IconName.ExclamationTriangleFill, ParentId = "5" }, + new (){ Id = "524", Text = "Tooltips", Href = "/tooltips", IconName = IconName.ChatSquareDotsFill, ParentId = "5" }, new (){ Id = "6", Text = "Data Visualization", IconName = IconName.BarChartFill, IconColor = IconColor.Warning }, new (){ Id = "600", Text = "Bar Chart", Href = "/charts/bar-chart", IconName = IconName.BarChartFill, ParentId = "6", Match = NavLinkMatch.All }, diff --git a/blazorbootstrap/Components/Spinner/Spinner.razor b/blazorbootstrap/Components/Spinner/Spinner.razor new file mode 100644 index 000000000..01424d629 --- /dev/null +++ b/blazorbootstrap/Components/Spinner/Spinner.razor @@ -0,0 +1,24 @@ +@namespace BlazorBootstrap +@inherits BlazorBootstrapComponentBase + +@if (Visible) +{ + @if (Type == SpinnerType.Dots) + { + + @if (!string.IsNullOrWhiteSpace(Title)) + { + @Title + } + + + + + } + else + { +
+ @VisuallyHiddenText +
+ } +} diff --git a/blazorbootstrap/Components/Spinner/Spinner.razor.cs b/blazorbootstrap/Components/Spinner/Spinner.razor.cs new file mode 100644 index 000000000..afaf82437 --- /dev/null +++ b/blazorbootstrap/Components/Spinner/Spinner.razor.cs @@ -0,0 +1,138 @@ +namespace BlazorBootstrap; + +public partial class Spinner : BlazorBootstrapComponentBase +{ + #region Fields and Constants + + private SpinnerColor color = SpinnerColor.None; + + private SpinnerType type = SpinnerType.Border; + + #endregion + + #region Methods + + /// + protected override void BuildClasses(CssClassBuilder builder) + { + builder.Append(BootstrapClassProvider.Spinner(Type)); + builder.Append(BootstrapClassProvider.Spinner(Color)); + builder.Append(BootstrapClassProvider.Spinner(Type, Size), Type is (SpinnerType.Border or SpinnerType.Grow)); + + base.BuildClasses(builder); + } + + protected override void OnInitialized() + { + Attributes ??= new Dictionary(); + + if (Type != SpinnerType.Dots) + { + if (string.IsNullOrWhiteSpace(Title)) + Attributes.Remove("title"); + else if (!Attributes.TryGetValue("title", out _)) + Attributes.Add("title", Title); + else if (Attributes.TryGetValue("title", out _)) + Attributes["title"] = Title; + } + + base.OnInitialized(); + } + + /// + /// Calculates width, height, and circles information for the spinner SVG. + /// + /// A tuple containing width, height, and a list of spinner circles. + private (int Width, int Height, List Circles) GetSpinnerSvgInfo() + { + // Calculate radius based on Size + var radius = 4; // default: SpinnerSize.Medium + + if (Size == SpinnerSize.Small) + radius = 2; + else if (Size == SpinnerSize.Large) + radius = 6; + else if (Size == SpinnerSize.ExtraLarge) + radius = 8; + + var defaultSpace = 4; + + // Calculate other dimensions based on radius + var diameter = 2 * radius; + + var circle1 = new SpinnerCircle(radius, radius, diameter); + var circle2 = new SpinnerCircle(radius, circle1.Cx + diameter + defaultSpace, diameter); + var circle3 = new SpinnerCircle(radius, circle2.Cx + diameter + defaultSpace, diameter); + + var width = defaultSpace + diameter * 3 + defaultSpace; + var height = defaultSpace + diameter + defaultSpace; + + return (width, height, new List { circle1, circle2, circle3 }); + } + + #endregion + + #region Properties, Indexers + + /// + protected override bool ShouldAutoGenerateId => true; + + /// + /// Gets or sets the color of the spinner. + /// + [Parameter] + public SpinnerColor Color + { + get => color; + set + { + color = value; + DirtyClasses(); + } + } + + /// + /// Gets or sets the size of the spinner. + /// + [Parameter] + public SpinnerSize Size { get; set; } = SpinnerSize.Medium; + + /// + /// Gets the width, height, and circles information for the spinner SVG. + /// + private (int Width, int Height, List Circles) SpinnerSvg => GetSpinnerSvgInfo(); + + /// + /// Gets or sets the title text used as an accessibility attribute. + /// + [Parameter] + public string? Title { get; set; } + + /// + /// Gets or sets the type of the spinner. + /// + [Parameter] + public SpinnerType Type + { + get => type; + set + { + type = value; + DirtyClasses(); + } + } + + /// + /// Gets or sets whether the spinner is visible or not. + /// + [Parameter] + public bool Visible { get; set; } = true; + + /// + /// Gets or sets the visually hidden text. + /// + [Parameter] + public string? VisuallyHiddenText { get; set; } = "Loading..."; + + #endregion +} diff --git a/blazorbootstrap/Components/Spinner/Spinner.razor.css b/blazorbootstrap/Components/Spinner/Spinner.razor.css new file mode 100644 index 000000000..b7e248257 --- /dev/null +++ b/blazorbootstrap/Components/Spinner/Spinner.razor.css @@ -0,0 +1,88 @@ +/* scss-docs-start spinner-dots-keyframes */ +@keyframes spinner-dots { + 0% { + opacity: 1; + } + + 50%, 100% { + opacity: .3; + } +} + +/* scss-docs-end spinner-dots-keyframes */ +.spinner-dots { + user-select: none; +} + + .spinner-dots > circle:first-of-type, + .spinner-dots > circle:last-of-type, + .spinner-dots > circle:nth-of-type(2) { + fill: currentcolor; + animation: spinner-dots .75s infinite ease-in-out alternate; + } + + .spinner-dots > circle:nth-of-type(2) { + animation-delay: .25s; + } + + .spinner-dots > circle:last-of-type { + animation-delay: .5s; + } + +/* Size: START */ + +/* default */ +.spinner-border-sm { + --bs-spinner-width: 1rem; + --bs-spinner-height: 1rem; + --bs-spinner-border-width: 0.25em; + --bs-spinner-vertical-align: -0.125em; +} +/* custom CSS classes */ +.spinner-border-md { + --bs-spinner-width: 2rem; + --bs-spinner-height: 2rem; + --bs-spinner-border-width: 0.25em; + --bs-spinner-vertical-align: -0.125em; +} +.spinner-border-lg { + --bs-spinner-width: 3rem; + --bs-spinner-height: 3rem; + --bs-spinner-border-width: 0.25em; + --bs-spinner-vertical-align: -0.125em; +} +.spinner-border-xl { + --bs-spinner-width: 4rem; + --bs-spinner-height: 4rem; + --bs-spinner-border-width: 0.25em; + --bs-spinner-vertical-align: -0.125em; +} + +/* default */ +.spinner-grow-sm { + --bs-spinner-width: 1rem; + --bs-spinner-height: 1rem; + --bs-spinner-border-width: 0.25em; + --bs-spinner-vertical-align: -0.125em; +} +/* custom CSS classes */ +.spinner-grow-md { + --bs-spinner-width: 2rem; + --bs-spinner-height: 2rem; + --bs-spinner-border-width: 0.25em; + --bs-spinner-vertical-align: -0.125em; +} +.spinner-grow-lg { + --bs-spinner-width: 3rem; + --bs-spinner-height: 3rem; + --bs-spinner-border-width: 0.25em; + --bs-spinner-vertical-align: -0.125em; +} +.spinner-grow-xl { + --bs-spinner-width: 4rem; + --bs-spinner-height: 4rem; + --bs-spinner-border-width: 0.25em; + --bs-spinner-vertical-align: -0.125em; +} + +/* Size: END */ \ No newline at end of file diff --git a/blazorbootstrap/Enums/SpinnerSize.cs b/blazorbootstrap/Enums/SpinnerSize.cs new file mode 100644 index 000000000..9fc6f1708 --- /dev/null +++ b/blazorbootstrap/Enums/SpinnerSize.cs @@ -0,0 +1,24 @@ +namespace BlazorBootstrap; + +public enum SpinnerSize +{ + /// + /// Makes an element small size. + /// + Small, + + /// + /// Makes an element medium size. + /// + Medium, + + /// + /// Makes an element large. + /// + Large, + + /// + /// Makes an element extra large. + /// + ExtraLarge +} diff --git a/blazorbootstrap/Enums/SpinnerType.cs b/blazorbootstrap/Enums/SpinnerType.cs new file mode 100644 index 000000000..ab1968da5 --- /dev/null +++ b/blazorbootstrap/Enums/SpinnerType.cs @@ -0,0 +1,8 @@ +namespace BlazorBootstrap; + +public enum SpinnerType +{ + Border, + Grow, + Dots +} diff --git a/blazorbootstrap/Models/SpinnerCircle.cs b/blazorbootstrap/Models/SpinnerCircle.cs new file mode 100644 index 000000000..5e82c6e2b --- /dev/null +++ b/blazorbootstrap/Models/SpinnerCircle.cs @@ -0,0 +1,3 @@ +namespace BlazorBootstrap; + +public record SpinnerCircle(int Radius, int Cx, int Cy); \ No newline at end of file diff --git a/blazorbootstrap/Utilities/BootstrapClassProvider.cs b/blazorbootstrap/Utilities/BootstrapClassProvider.cs index d38306d60..4d4637642 100644 --- a/blazorbootstrap/Utilities/BootstrapClassProvider.cs +++ b/blazorbootstrap/Utilities/BootstrapClassProvider.cs @@ -169,6 +169,11 @@ public class BootstrapClassProvider public string ProgressBarStriped() => $"{ProgressBar()}-striped"; public string Show() => "show"; + + public string Spinner() => "spinner"; + public string Spinner(SpinnerColor color) => ToSpinnerColor(color)!; + public string Spinner(SpinnerType type) => $"{Spinner()}-{ToSpinnerType(type)}"; + public string Spinner(SpinnerType type, SpinnerSize size) => $"{Spinner(type)}-{ToSpinnerSize(size)}"; public string Table() => "table"; public string TableActive() => "table-active"; @@ -601,6 +606,39 @@ public string ToPosition(Position position) => _ => null }; + public string? ToSpinnerColor(SpinnerColor color) => + color switch + { + SpinnerColor.Primary => "text-primary", + SpinnerColor.Secondary => "text-secondary", + SpinnerColor.Success => "text-success", + SpinnerColor.Danger => "text-danger", + SpinnerColor.Warning => "text-warning", + SpinnerColor.Info => "text-info", + SpinnerColor.Light => "text-light", + SpinnerColor.Dark => "text-dark", + _ => null + }; + + public string ToSpinnerSize(SpinnerSize size) => + size switch + { + SpinnerSize.Small => "sm", + SpinnerSize.Medium => "md", + SpinnerSize.Large => "lg", + SpinnerSize.ExtraLarge => "xl", + _ => "md" + }; + + public string ToSpinnerType(SpinnerType type) => + type switch + { + SpinnerType.Border => "border", + SpinnerType.Grow => "grow", + SpinnerType.Dots => "dots", + _ => "border" + }; + public string? ToTabColor(TabColor color) => color switch { diff --git a/blazorbootstrap/Utilities/BootstrapStyleProvider.cs b/blazorbootstrap/Utilities/BootstrapStyleProvider.cs new file mode 100644 index 000000000..9a9a92cea --- /dev/null +++ b/blazorbootstrap/Utilities/BootstrapStyleProvider.cs @@ -0,0 +1,10 @@ +namespace BlazorBootstrap; + +public class BootstrapStyleProvider +{ + #region Methods + + // TODO: place holder for custom styles + + #endregion +} diff --git a/docs/blog/2024-02-04-blazorbootstrap-1.11.0.md b/docs/blog/2024-02-04-blazorbootstrap-1.11.0.md index ef154472e..a37c2e6eb 100644 --- a/docs/blog/2024-02-04-blazorbootstrap-1.11.0.md +++ b/docs/blog/2024-02-04-blazorbootstrap-1.11.0.md @@ -16,15 +16,15 @@ We are excited to release version 1.11.0, featuring new PDF Viewer, Range Input, ## What's New -- 'PDF Viewer' component +- `PDF Viewer` component - Allows users to view PDF files directly in the browser, eliminating the need for third-party browser tools or extensions. - Supports two callback events: OnDocumentLoaded and OnPageChanged -- 'Range Input' component +- `Range Input` component - Disabled - Min and Max - Step - Tick marks -- 'Script Loader' component +- `Script Loader` component - Allows users to load JS sctipt files dynamically on the fly. ## What's changed