diff --git a/.github/workflows/bit.full.ci.yml b/.github/workflows/bit.full.ci.yml index 2143f22a84..7e34849b23 100644 --- a/.github/workflows/bit.full.ci.yml +++ b/.github/workflows/bit.full.ci.yml @@ -480,6 +480,14 @@ jobs: dotnet build src/BlazorUI/Bit.BlazorUI.Extras/Bit.BlazorUI.Extras.csproj -c Release -p:GeneratePackageOnBuild=false -p:WarningLevel=0 -p:RunCodeAnalysis=false dotnet pack src/BlazorUI/Bit.BlazorUI.Extras/Bit.BlazorUI.Extras.csproj --output ./packages --configuration Release + - name: Generate CSS/JS files BlazorUI.Legacy + run: dotnet build src/BlazorUI/Bit.BlazorUI.Legacy/Bit.BlazorUI.Legacy.csproj -t:BeforeBuildTasks --no-restore -f:net10.0 -c Release + + - name: Build and pack BlazorUI.Legacy + run: | + dotnet build src/BlazorUI/Bit.BlazorUI.Legacy/Bit.BlazorUI.Legacy.csproj -c Release -p:GeneratePackageOnBuild=false -p:WarningLevel=0 -p:RunCodeAnalysis=false + dotnet pack src/BlazorUI/Bit.BlazorUI.Legacy/Bit.BlazorUI.Legacy.csproj --output ./packages --configuration Release + - name: Generate CSS/JS files BlazorUI.Assets run: dotnet build src/BlazorUI/Bit.BlazorUI.Assets/Bit.BlazorUI.Assets.csproj -t:BeforeBuildTasks --no-restore -f:net10.0 -c Release diff --git a/.github/workflows/nuget.org.yml b/.github/workflows/nuget.org.yml index d13e9c30af..f5f2594a8f 100644 --- a/.github/workflows/nuget.org.yml +++ b/.github/workflows/nuget.org.yml @@ -103,6 +103,13 @@ jobs: - name: dotnet pack BlazorUI.Extras run: dotnet pack src/BlazorUI/Bit.BlazorUI.Extras/Bit.BlazorUI.Extras.csproj --output . --configuration Release + - name: Generate CSS/JS files BlazorUI.Legacy + run: dotnet build src/BlazorUI/Bit.BlazorUI.Legacy/Bit.BlazorUI.Legacy.csproj -t:BeforeBuildTasks --no-restore -f:net10.0 -c Release + - name: dotnet build BlazorUI.Legacy + run: dotnet build src/BlazorUI/Bit.BlazorUI.Legacy/Bit.BlazorUI.Legacy.csproj -c Release -p:GeneratePackageOnBuild=false -p:WarningLevel=0 -p:RunCodeAnalysis=false + - name: dotnet pack BlazorUI.Legacy + run: dotnet pack src/BlazorUI/Bit.BlazorUI.Legacy/Bit.BlazorUI.Legacy.csproj --output . --configuration Release + - name: Generate CSS/JS files BlazorUI.Assets run: dotnet build src/BlazorUI/Bit.BlazorUI.Assets/Bit.BlazorUI.Assets.csproj -t:BeforeBuildTasks --no-restore -f:net10.0 -c Release - name: dotnet build BlazorUI.Assets diff --git a/.github/workflows/prerelease.nuget.org.yml b/.github/workflows/prerelease.nuget.org.yml index 9e8371bcc8..4a41d6140f 100644 --- a/.github/workflows/prerelease.nuget.org.yml +++ b/.github/workflows/prerelease.nuget.org.yml @@ -92,6 +92,13 @@ jobs: - name: dotnet pack BlazorUI.Extras run: dotnet pack src/BlazorUI/Bit.BlazorUI.Extras/Bit.BlazorUI.Extras.csproj --output . --configuration Release + - name: Generate CSS/JS files BlazorUI.Legacy + run: dotnet build src/BlazorUI/Bit.BlazorUI.Legacy/Bit.BlazorUI.Legacy.csproj -t:BeforeBuildTasks --no-restore -f:net10.0 -c Release + - name: dotnet build BlazorUI.Legacy + run: dotnet build src/BlazorUI/Bit.BlazorUI.Legacy/Bit.BlazorUI.Legacy.csproj -c Release -p:GeneratePackageOnBuild=false -p:WarningLevel=0 -p:RunCodeAnalysis=false + - name: dotnet pack BlazorUI.Legacy + run: dotnet pack src/BlazorUI/Bit.BlazorUI.Legacy/Bit.BlazorUI.Legacy.csproj --output . --configuration Release + - name: Generate CSS/JS files BlazorUI.Assets run: dotnet build src/BlazorUI/Bit.BlazorUI.Assets/Bit.BlazorUI.Assets.csproj -t:BeforeBuildTasks --no-restore -f:net10.0 -c Release - name: dotnet build BlazorUI.Assets diff --git a/.gitignore b/.gitignore index f2f3953377..85aabc07d8 100644 --- a/.gitignore +++ b/.gitignore @@ -251,6 +251,12 @@ _book !/src/BlazorUI/Bit.BlazorUI.Extras/wwwroot/quilljs/*.css !/src/BlazorUI/Bit.BlazorUI.Extras/wwwroot/leaflet/*.css +/src/BlazorUI/Bit.BlazorUI.Legacy/**/*.css +/src/BlazorUI/Bit.BlazorUI.Legacy/Scripts/*.js +/src/BlazorUI/Bit.BlazorUI.Legacy/wwwroot/scripts/*.js +/src/BlazorUI/Bit.BlazorUI.Legacy/wwwroot/**/*.css.gz +!/src/BlazorUI/Bit.BlazorUI.Legacy/wwwroot/quilljs/*.css + /src/BlazorUI/Bit.BlazorUI.Assets/**/*.css /src/BlazorUI/Bit.BlazorUI.Assets/wwwroot/scripts/*.js /src/BlazorUI/Bit.BlazorUI.Assets/wwwroot/**/*.css.gz diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Bit.BlazorUI.Legacy.csproj b/src/BlazorUI/Bit.BlazorUI.Legacy/Bit.BlazorUI.Legacy.csproj new file mode 100644 index 0000000000..077a29d059 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Bit.BlazorUI.Legacy.csproj @@ -0,0 +1,87 @@ + + + + + + net10.0;net9.0;net8.0 + true + enable + + BeforeBuildTasks; + $(ResolveStaticWebAssetsInputsDependsOn) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/BitChartBarCategoryAxis.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/BitChartBarCategoryAxis.cs new file mode 100644 index 0000000000..3e525586ac --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/BitChartBarCategoryAxis.cs @@ -0,0 +1,74 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Extended version of for use in a bar chart. +/// As per documentation here (Chart.js). +/// +public class BitChartBarCategoryAxis : BitChartCategoryAxis +{ + /// + /// Gets or sets the percentage (0-1) of the available width each bar should be within the category width. + /// 1.0 will take the whole category width and put the bars right next to each other. + /// + public double? BarPercentage { get; set; } + + /// + /// Gets or sets the percentage (0-1) of the available width each category should be within the sample width. + /// + public double? CategoryPercentage { get; set; } + + /// + /// Gets or sets the width of each bar in pixels. + /// If set to , it computes "optimal" sample widths that globally arrange bars side by side. If not set (default), bars are equally sized based on the smallest interval. + /// + public BitChartBarThickness BarThickness { get; set; } = default!; + + /// + /// Gets or sets the maximum bar thickness. + /// Set this to ensure that bars are not sized thicker than this. + /// + public double? MaxBarThickness { get; set; } + + /// + /// Gets or sets the minimum bar length. + /// Set this to ensure that bars have a minimum length in pixels. + /// + public double? MinBarLength { get; set; } + + /// + /// Gets or sets a value indicating whether grid lines will be shifted to be between labels or not. + /// If , the bars for a particular data point fall between the grid lines. + /// The grid line will move to the left by one half of the tick interval. + /// If , the grid line will go right down the middle of the bars. + /// Changing this value will directly affect of the property in this instance. + /// + public bool? OffsetGridLines + { + get => GridLines?.OffsetGridLines; + set + { + if (GridLines == null) + { + if (value == null) + { + return; + } + else + { + GridLines = new BitChartGridLines(); + } + } + + GridLines.OffsetGridLines = value; + } + } + + /// + /// Gets or sets a value indicating whether the bar chart is stacked or not. + /// Bar charts can be configured into stacked bar charts by changing the settings on the X and Y axes to enable stacking. + /// Stacked bar charts can be used to show how one data series is made up of a number of smaller pieces. + /// As per documentation here (Chart.js). + /// + public bool? Stacked { get; set; } +} + diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/BitChartBarLinearCartesianAxis.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/BitChartBarLinearCartesianAxis.cs new file mode 100644 index 0000000000..04519e4ed0 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/BitChartBarLinearCartesianAxis.cs @@ -0,0 +1,74 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Extended version of for use in a bar chart. +/// As per documentation here (Chart.js). +/// +public class BitChartBarLinearCartesianAxis : BitChartLinearCartesianAxis +{ + /// + /// Gets or sets the percentage (0-1) of the available width each bar should be within the category width. + /// 1.0 will take the whole category width and put the bars right next to each other. + /// + public double? BarPercentage { get; set; } + + /// + /// Gets or sets the percentage (0-1) of the available width each category should be within the sample width. + /// + public double? CategoryPercentage { get; set; } + + /// + /// Gets or sets the width of each bar in pixels. + /// If set to , it computes "optimal" sample widths that globally arrange bars side by side. If not set (default), bars are equally sized based on the smallest interval. + /// + public BitChartBarThickness BarThickness { get; set; } = default!; + + /// + /// Gets or sets the maximum bar thickness. + /// Set this to ensure that bars are not sized thicker than this. + /// + public double? MaxBarThickness { get; set; } + + /// + /// Gets or sets the minimum bar length. + /// Set this to ensure that bars have a minimum length in pixels. + /// + public double? MinBarLength { get; set; } + + /// + /// Gets or sets a value indicating whether grid lines will be shifted to be between labels or not. + /// If , the bars for a particular data point fall between the grid lines. + /// The grid line will move to the left by one half of the tick interval. + /// If , the grid line will go right down the middle of the bars. + /// Changing this value will directly affect of the property in this instance. + /// + public bool? OffsetGridLines + { + get => GridLines?.OffsetGridLines; + set + { + if (GridLines == null) + { + if (value == null) + { + return; + } + else + { + GridLines = new BitChartGridLines(); + } + } + + GridLines.OffsetGridLines = value; + } + } + + /// + /// Gets or sets a value indicating whether the bar chart is stacked or not. + /// Bar charts can be configured into stacked bar charts by changing the settings on the X and Y axes to enable stacking. + /// Stacked bar charts can be used to show how one data series is made up of a number of smaller pieces. + /// As per documentation here (Chart.js). + /// + public bool? Stacked { get; set; } +} + diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/BitChartBarLogarithmicAxis.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/BitChartBarLogarithmicAxis.cs new file mode 100644 index 0000000000..5e82e14063 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/BitChartBarLogarithmicAxis.cs @@ -0,0 +1,74 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Extended version of for use in a bar chart. +/// As per documentation here (Chart.js). +/// +public class BitChartBarLogarithmicAxis : BitChartLogarithmicAxis +{ + /// + /// Gets or sets the percentage (0-1) of the available width each bar should be within the category width. + /// 1.0 will take the whole category width and put the bars right next to each other. + /// + public double? BarPercentage { get; set; } + + /// + /// Gets or sets the percentage (0-1) of the available width each category should be within the sample width. + /// + public double? CategoryPercentage { get; set; } + + /// + /// Gets or sets the width of each bar in pixels. + /// If set to , it computes "optimal" sample widths that globally arrange bars side by side. If not set (default), bars are equally sized based on the smallest interval. + /// + public BitChartBarThickness BarThickness { get; set; } = default!; + + /// + /// Gets or sets the maximum bar thickness. + /// Set this to ensure that bars are not sized thicker than this. + /// + public double? MaxBarThickness { get; set; } + + /// + /// Gets or sets the minimum bar length. + /// Set this to ensure that bars have a minimum length in pixels. + /// + public double? MinBarLength { get; set; } + + /// + /// Gets or sets a value indicating whether grid lines will be shifted to be between labels or not. + /// If , the bars for a particular data point fall between the grid lines. + /// The grid line will move to the left by one half of the tick interval. + /// If , the grid line will go right down the middle of the bars. + /// Changing this value will directly affect of the property in this instance. + /// + public bool? OffsetGridLines + { + get => GridLines?.OffsetGridLines; + set + { + if (GridLines == null) + { + if (value == null) + { + return; + } + else + { + GridLines = new BitChartGridLines(); + } + } + + GridLines.OffsetGridLines = value; + } + } + + /// + /// Gets or sets a value indicating whether the bar chart is stacked or not. + /// Bar charts can be configured into stacked bar charts by changing the settings on the X and Y axes to enable stacking. + /// Stacked bar charts can be used to show how one data series is made up of a number of smaller pieces. + /// As per documentation here (Chart.js). + /// + public bool? Stacked { get; set; } +} + diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/BitChartBarTimeAxis.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/BitChartBarTimeAxis.cs new file mode 100644 index 0000000000..e14745b0a0 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/BitChartBarTimeAxis.cs @@ -0,0 +1,74 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Extended version of for use in a bar chart. +/// As per documentation here (Chart.js). +/// +public class BitChartBarTimeAxis : BitChartTimeAxis +{ + /// + /// Gets or sets the percentage (0-1) of the available width each bar should be within the category width. + /// 1.0 will take the whole category width and put the bars right next to each other. + /// + public double? BarPercentage { get; set; } + + /// + /// Gets or sets the percentage (0-1) of the available width each category should be within the sample width. + /// + public double? CategoryPercentage { get; set; } + + /// + /// Gets or sets the width of each bar in pixels. + /// If set to , it computes "optimal" sample widths that globally arrange bars side by side. If not set (default), bars are equally sized based on the smallest interval. + /// + public BitChartBarThickness BarThickness { get; set; } = default!; + + /// + /// Gets or sets the maximum bar thickness. + /// Set this to ensure that bars are not sized thicker than this. + /// + public double? MaxBarThickness { get; set; } + + /// + /// Gets or sets the minimum bar length. + /// Set this to ensure that bars have a minimum length in pixels. + /// + public double? MinBarLength { get; set; } + + /// + /// Gets or sets a value indicating whether grid lines will be shifted to be between labels or not. + /// If , the bars for a particular data point fall between the grid lines. + /// The grid line will move to the left by one half of the tick interval. + /// If , the grid line will go right down the middle of the bars. + /// Changing this value will directly affect of the property in this instance. + /// + public bool? OffsetGridLines + { + get => GridLines?.OffsetGridLines; + set + { + if (GridLines == null) + { + if (value == null) + { + return; + } + else + { + GridLines = new BitChartGridLines(); + } + } + + GridLines.OffsetGridLines = value; + } + } + + /// + /// Gets or sets a value indicating whether the bar chart is stacked or not. + /// Bar charts can be configured into stacked bar charts by changing the settings on the X and Y axes to enable stacking. + /// Stacked bar charts can be used to show how one data series is made up of a number of smaller pieces. + /// As per documentation here (Chart.js). + /// + public bool? Stacked { get; set; } +} + diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/ExtendedCartesianAxes.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/ExtendedCartesianAxes.cs new file mode 100644 index 0000000000..5f282702bb --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/ExtendedCartesianAxes.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/ExtendedCartesianAxes.tt b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/ExtendedCartesianAxes.tt new file mode 100644 index 0000000000..0919e2c04b --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/Axes/ExtendedCartesianAxes.tt @@ -0,0 +1,102 @@ +<#@ template debug="false" hostspecific="true" language="C#" #> +<#@ output extension=".cs" #> +<#@ assembly name="System.Core" #> +<#@ assembly name="$(TargetPath)" #> +<#@ import namespace="System.IO" #> + +<# + string[] cartesianAxesNames = new[] { "CategoryAxis", "LinearCartesianAxis", "LogarithmicAxis", "TimeAxis" }; + foreach (string baseName in cartesianAxesNames) + { + string className = "BitChartBar" + baseName; +#> + +namespace Bit.BlazorUI +{ + /// + /// Extended version of for use in a bar chart. + /// As per documentation here (Chart.js). + /// + public class <#= className #> : <#= baseName #> + { + /// + /// Gets or sets the percentage (0-1) of the available width each bar should be within the category width. + /// 1.0 will take the whole category width and put the bars right next to each other. + /// + public double? BarPercentage { get; set; } + + /// + /// Gets or sets the percentage (0-1) of the available width each category should be within the sample width. + /// + public double? CategoryPercentage { get; set; } + + /// + /// Gets or sets the width of each bar in pixels. + /// If set to , it computes "optimal" sample widths that globally arrange bars side by side. If not set (default), bars are equally sized based on the smallest interval. + /// + public BarThickness BarThickness { get; set; } + + /// + /// Gets or sets the maximum bar thickness. + /// Set this to ensure that bars are not sized thicker than this. + /// + public double? MaxBarThickness { get; set; } + + /// + /// Gets or sets the minimum bar length. + /// Set this to ensure that bars have a minimum length in pixels. + /// + public double? MinBarLength { get; set; } + + /// + /// Gets or sets a value indicating whether grid lines will be shifted to be between labels or not. + /// If , the bars for a particular data point fall between the grid lines. + /// The grid line will move to the left by one half of the tick interval. + /// If , the grid line will go right down the middle of the bars. + /// Changing this value will directly affect of the property in this instance. + /// + public bool? OffsetGridLines + { + get => GridLines?.OffsetGridLines; + set + { + if (GridLines == null) + { + if (value == null) + { + return; + } + else + { + GridLines = new GridLines(); + } + } + + GridLines.OffsetGridLines = value; + } + } + + /// + /// Gets or sets a value indicating whether the bar chart is stacked or not. + /// Bar charts can be configured into stacked bar charts by changing the settings on the X and Y axes to enable stacking. + /// Stacked bar charts can be used to show how one data series is made up of a number of smaller pieces. + /// As per documentation here (Chart.js). + /// + public bool? Stacked { get; set; } + } +} + +<# + // End of file. + SaveOutput($"{className}.cs"); +} +#> +<#+ +private void SaveOutput(string outputFileName) +{ + string templateDirectory = Path.GetDirectoryName(Host.TemplateFile); + string outputFilePath = Path.Combine(templateDirectory, outputFileName); + File.WriteAllText(outputFilePath, this.GenerationEnvironment.ToString()); + this.GenerationEnvironment.Remove(0, this.GenerationEnvironment.Length); +} +#> diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartBarConfig.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartBarConfig.cs new file mode 100644 index 0000000000..99f8b16b9b --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartBarConfig.cs @@ -0,0 +1,18 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents the config for a bar chart. +/// +public class BitChartBarConfig : BitChartConfigBase +{ + /// + /// Creates a new instance of the class. + /// + /// + /// If , the chart-type will be set to + /// instead of which turns this chart into a horizontal + /// bar chart. If set to , you also have to pass in for + /// the s. + /// + public BitChartBarConfig(bool horizontal = false) : base(horizontal ? BitChartChartType.HorizontalBar : BitChartChartType.Bar) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartBarDataset.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartBarDataset.cs new file mode 100644 index 0000000000..5b82398e4c --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartBarDataset.cs @@ -0,0 +1,150 @@ +using Newtonsoft.Json; + +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a dataset for a bar chart. +/// As per documentation here (Chart.js). +/// +/// The type of data this contains. +public class BitChartBarDataset : BitChartDataset +{ + /// + /// Creates a new instance of . + /// + /// + /// If , the dataset-type will be set to + /// instead of . Set this to when using a horizontal + /// bar chart. If this is set to in a horizontal bar chart, the bars won't be displayed. + /// + public BitChartBarDataset(bool horizontal = false) : base(horizontal ? BitChartChartType.HorizontalBar : BitChartChartType.Bar) { } + + /// + /// Creates a new instance of with initial data. + /// + /// + public BitChartBarDataset(IEnumerable data, bool horizontal = false) : this(horizontal) + { + AddRange(data); + } + + /// + /// Creates a new instance of with + /// a custom . Use this constructor when + /// you implement a bar-like chart. + /// + /// The to use instead of . + protected BitChartBarDataset(BitChartChartType type) : base(type) { } + + /// + /// Gets or sets the fill color of the bars in the dataset. + /// See for working with colors. + /// + public BitChartIndexableOption? BackgroundColor { get; set; } + + /// + /// Gets or sets the border color of the bars in the dataset. + /// See for working with colors. + /// + public BitChartIndexableOption? BorderColor { get; set; } + + /// + /// Gets or sets a value to avoid drawing the bar stroke at the base of the fill. + /// In general, this does not need to be changed except when creating chart types that derive from a bar chart. + /// + public BitChartIndexableOption? BorderSkipped { get; set; } + + /// + /// Gets or sets the border width of the bars in the dataset. + /// + public BitChartIndexableOption? BorderWidth { get; set; } + + /// + /// Gets or sets the percentage (0-1) of the available width each bar should be within the category + /// width. 1.0 will take the whole category width and put the bars right next to each other. + /// As per documentation here (Chart.js). + /// + public double? BarPercentage { get; set; } + + /// + /// Gets or sets the percentage (0-1) of the available width each category should be within the sample width. + /// As per documentation here (Chart.js).. + /// + public double? CategoryPercentage { get; set; } + + /// + /// Gets or sets the width of each bar in pixels. + /// If set to , it computes "optimal" sample widths that globally + /// arrange bars side by side. If not set (default), bars are equally sized based on the smallest interval. + /// + public BitChartBarThickness? BarThickness { get; set; } + + /// + /// Gets or sets how to clip relative to the chart area. Positive values allow overflow, + /// negative values clip that many pixels inside the chart area. + /// + public BitChartClipping? Clip { get; set; } + + /// + /// Gets or sets the fill color of the bars when hovered. + /// See for working with colors. + /// + public BitChartIndexableOption? HoverBackgroundColor { get; set; } + + /// + /// Gets or sets the stroke color of the bars when hovered. + /// See for working with colors. + /// + public BitChartIndexableOption? HoverBorderColor { get; set; } + + /// + /// Gets or sets the stroke width of the bars when hovered. + /// + public BitChartIndexableOption? HoverBorderWidth { get; set; } + + /// + /// Gets or sets the label for the dataset which appears in the legend and tooltips. + /// + public string? Label { get; set; } + + /// + /// Gets or sets the maximum thickness of the bars in pixels. + /// + public double? MaxBarThickness { get; set; } + + /// + /// Gets or sets the minimum length of the bars in pixels. + /// + public double? MinBarLength { get; set; } + + /// + /// Gets or sets the id of the group to which this dataset belongs to + /// (when stacked, each group will be a separate stack). + /// + /// In order to use this, the 'Stacked' property of the corresponding + /// axis has to be set to . The 'Stacked' property + /// is only available in axes. + /// + /// + public string? Stack { get; set; } + + /// + /// Gets or sets the drawing order of this dataset. + /// Also affects the order for stacking, tooltips, and the legend. + /// + public int? Order { get; set; } + + /// + /// Gets or sets the ID of the x axis to plot this dataset on. If not specified, + /// this defaults to the ID of the first found x axis. + /// + [JsonProperty("xAxisID")] + public string? XAxisId { get; set; } + + /// + /// Gets or sets the ID of the y axis to plot this dataset on. If not specified, + /// this defaults to the ID of the first found y axis. + /// + [JsonProperty("yAxisID")] + public string? YAxisId { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartBarOptions.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartBarOptions.cs new file mode 100644 index 0000000000..86927f5a16 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartBarOptions.cs @@ -0,0 +1,12 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The options-subconfig of a . +/// +public class BitChartBarOptions : BitChartBaseConfigOptions +{ + /// + /// Gets or sets the scales for this chart. + /// + public BitChartBarScales? Scales { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartBarScales.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartBarScales.cs new file mode 100644 index 0000000000..3782f6cdde --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartBarScales.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace Bit.BlazorUI.Legacy; + +/// +/// Defines the scales for bar charts by holding the x and y axes. +/// +public class BitChartBarScales +{ + /// + /// Gets or sets the configurations for the x-axes. + /// You can use any but there are extended axes which contain additional properties to customize the bar chart axes. + /// + [JsonProperty("xAxes")] + public List? XAxes { get; set; } + + /// + /// Gets or sets the configurations for the y-axes. + /// You can use any but there are extended axes which contain additional properties to customize the bar chart axes. + /// + [JsonProperty("yAxes")] + public List? YAxes { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartBarThickness.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartBarThickness.cs new file mode 100644 index 0000000000..d168874ba3 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartBarThickness.cs @@ -0,0 +1,39 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The bar thickness used to customize all bar axes. +/// As per documentation here (Chart.js). +/// +public class BitChartBarThickness : BitChartObjectEnum +{ + /// + /// Creates a new instance of the class. + /// + /// The value to set. + private BitChartBarThickness(string stringValue) : base(stringValue) { } + + /// + /// Creates a new instance of the class. + /// + /// The value to set. + private BitChartBarThickness(double doubleValue) : base(doubleValue) { } + + /// + /// Gets a from a value (in pixels). + /// + /// The thickness value in pixels. + /// A from a value (in pixels). + public static BitChartBarThickness Absolute(double thickness) => new BitChartBarThickness(thickness); + + /// + /// Converts a value to a value. + /// + /// The thickness value in pixels. + public static implicit operator BitChartBarThickness(double thickness) => new BitChartBarThickness(thickness); + + /// + /// If set to , the base sample widths are calculated automatically + /// based on the previous and following samples so that they take the full available widths without overlap. + /// + public static BitChartBarThickness Flex => new BitChartBarThickness("flex"); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartFloatingBarPoint.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartFloatingBarPoint.cs new file mode 100644 index 0000000000..dd32a354a3 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/BitChartFloatingBarPoint.cs @@ -0,0 +1,45 @@ +using Newtonsoft.Json; + +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a floating bar within a bar chart (use in ). +/// Using this type, the bars will be rendered with gaps between them (floating-bars). +/// +/// When serialized, this value is represented as an array of two numbers. +/// will be the first number in the array, the second one. +/// +/// +[JsonConverter(typeof(FloatingBarPointConverter))] +public readonly struct BitChartFloatingBarPoint : IEquatable +{ + /// + /// Gets the start-value of this . + /// When serialized, this will be the first value in the array. + /// + public double Start { get; } + + /// + /// Gets the end-value of this . + /// When serialized, this will be the second value in the array. + /// + public double End { get; } + + /// + /// Creates a new instance of . + /// + /// The start-value for this . + /// The end-value for this . + public BitChartFloatingBarPoint(double start, double end) + { + Start = start; + End = end; + } + + public override bool Equals(object? obj) => obj is BitChartFloatingBarPoint point && Equals(point); + public bool Equals(BitChartFloatingBarPoint other) => Start == other.Start && End == other.End; + public override int GetHashCode() => HashCode.Combine(Start, End); + + public static bool operator ==(BitChartFloatingBarPoint left, BitChartFloatingBarPoint right) => left.Equals(right); + public static bool operator !=(BitChartFloatingBarPoint left, BitChartFloatingBarPoint right) => !(left == right); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/FloatingBarPointConverter.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/FloatingBarPointConverter.cs new file mode 100644 index 0000000000..70c9b244e7 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BarChart/FloatingBarPointConverter.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace Bit.BlazorUI.Legacy; + +internal class FloatingBarPointConverter : JsonConverter +{ + public override BitChartFloatingBarPoint ReadJson(JsonReader reader, Type objectType, BitChartFloatingBarPoint existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.StartArray) + throw new JsonReaderException(); + + var arr = serializer.Deserialize(reader); + if (arr!.Length != 2) + throw new JsonReaderException(); + + return new BitChartFloatingBarPoint(arr[0], arr[1]); + } + + public override void WriteJson(JsonWriter writer, BitChartFloatingBarPoint value, JsonSerializer serializer) + { + writer.WriteStartArray(); + writer.WriteValue(value.Start); + writer.WriteValue(value.End); + writer.WriteEndArray(); + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BitChart.razor b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BitChart.razor new file mode 100644 index 0000000000..a50cf8fc8a --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BitChart.razor @@ -0,0 +1,3 @@ +@namespace Bit.BlazorUI.Legacy + + diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BitChart.razor.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BitChart.razor.cs new file mode 100644 index 0000000000..2a5db7bafa --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BitChart.razor.cs @@ -0,0 +1,183 @@ +// a fork from https://github.com/mariusmuntean/ChartJs.Blazor + +using System.Diagnostics.CodeAnalysis; + +namespace Bit.BlazorUI.Legacy; + +/// +/// Simple and flexible charting component for data visualization, which supports eight chart types: bar, line, area, pie, bubble, radar, polar, and scatter. +/// +public partial class BitChart : IAsyncDisposable +{ + private bool _disposed; + + + + [Inject] private IJSRuntime _js { get; set; } = default!; + + /// + /// The configuration of the chart. + /// + [Parameter] public BitChartConfigBase? Config { get; set; } + + /// + /// The height of the canvas HTML element. + /// Use when using . + /// + [Parameter] public int? Height { get; set; } + + /// + /// This event is fired when the chart has been setup through interop and + /// the JavaScript chart object is available. Use this callback if you need to setup + /// custom JavaScript options or register plugins. + /// + [Parameter] public EventCallback SetupCompletedCallback { get; set; } + + /// + /// The width of the canvas HTML element. + /// + [Parameter] public int? Width { get; set; } + + /// + /// Whether the date adapter is required for the current configuration. + /// By default BitChart uses the date-fns adapter. you can change the adapter using . + /// for more info check out https://www.chartjs.org/docs/2.9.4/axes/cartesian/time.html#date-adapters + /// + [Parameter] public bool IsDateAdapterRequired { get; set; } + + /// + /// The list of scripts required for the customized chartjs date adapter. + /// see available adapters here: https://github.com/chartjs/awesome#adapters + /// + [Parameter] public IEnumerable? DateAdapterScripts { get; set; } + + + + /// + /// Updates the chart. + /// + /// Call this method after you've updated the . + /// + /// + public Task Update() + { + if (Config is null) return Task.CompletedTask; + + return _js.BitChartJsUpdateChart(Config).AsTask(); + } + + + + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartBarConfig))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartBubbleConfig))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartLineConfig))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartPieConfig))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartPolarAreaConfig))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartRadarConfig))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartScatterConfig))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartConfigBase<,>))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartConfigBase<>))] + + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(JsonStringEnumConverter))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(IndexableOptionConverter))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FloatingBarPointConverter))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ClippingJsonConverter))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(JsonWriteOnlyConverter<>))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(JsonObjectEnumConverter))] + + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartBarOptions))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartBubbleOptions))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartBaseConfigOptions))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartLineOptions))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartPieOptions))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartPolarAreaOptions))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartRadarOptions))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartIndexableOption<>))] + + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartBarDataset<>))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartBubbleDataset))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartDataset<>))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartLineDataset<>))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartPieDataset))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartPolarAreaDataset))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartRadarDataset))] + + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartLegend))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartPosition))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartTooltips))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartAnimation))] + + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartBarScales))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartScales))] + + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartCartesianTicks))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartCategoryTicks))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartLinearCartesianTicks))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartLogarithmicTicks))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartTimeTicks))] + + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartBarCategoryAxis))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartBarLinearCartesianAxis))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartBarLogarithmicAxis))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartBarTimeAxis))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartCartesianAxis))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartCartesianAxis<>))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartCategoryAxis))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartLinearCartesianAxis))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartLogarithmicAxis))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitChartTimeAxis))] + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await _js.BitLegacyInitScripts(["_content/Bit.BlazorUI.Legacy/chart.js/chartjs-2.9.4.js"]); + + if (IsDateAdapterRequired && DateAdapterScripts is null) + { + await _js.BitLegacyInitScripts(["_content/Bit.BlazorUI.Legacy/chart.js/chartjs-2.9.4-adapter.js"]); + } + + if (DateAdapterScripts is not null) + { + await _js.BitLegacyInitScripts(DateAdapterScripts); + } + + if (Config is not null) + { + await _js.BitChartJsSetupChart(Config); + } + + await SetupCompletedCallback.InvokeAsync(this); + return; + } + + if (Config is not null) + { + await _js.BitChartJsSetupChart(Config); + } + } + + + + public async ValueTask DisposeAsync() + { + await DisposeAsync(true); + GC.SuppressFinalize(this); + } + + protected virtual async ValueTask DisposeAsync(bool disposing) + { + if (_disposed || disposing is false) return; + + try + { + if (Config is not null) + { + await _js.BitChartJsRemoveChart(Config.CanvasId); + } + } + catch (JSDisconnectedException) { } // we can ignore this exception here + + _disposed = true; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BitChart.ts b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BitChart.ts new file mode 100644 index 0000000000..85795c4131 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BitChart.ts @@ -0,0 +1,368 @@ +namespace BitBlazorUI.Legacy { + interface BitChartConfiguration extends Chart.ChartConfiguration { + canvasId: string; + } + + interface DotNetObjectReference { + invokeMethod(methodName: string, ...args: any[]): any; + invokeMethodAsync(methodName: string, ...args: any[]): Promise; + } + + // maybe the interop could be improved somehow by using generic similar to how it's done in C# + // but since we don't need typesafety in JavaScript and can't rely on it anyway (just like we + // have to invoke the C# delegate dynamically) we're not using generics. + export interface IMethodHandler { + methodName: string; + } + + interface DelegateHandler extends IMethodHandler { + handlerReference: DotNetObjectReference; + returnsValue: boolean; + ignoredIndices: number[]; + } + + export class BitChart { + private static _bitCharts = new Map(); + + public static getChartJs(canvasId: string) { + if (!BitChart._bitCharts.has(canvasId)) return null; + + return BitChart._bitCharts.get(canvasId)!; + } + + public static removeChart(canvasId: string) { + if (!BitChart._bitCharts.has(canvasId)) return; + + var chart = BitChart._bitCharts.get(canvasId)!; + chart.destroy(); + chart.config = {}; + BitChart._bitCharts.delete(canvasId); + } + + public static setupChart(config: BitChartConfiguration): boolean { + if (BitChart._bitCharts.has(config.canvasId)) { + return BitChart.updateChart(config); + } else { + BitChart.wireUpCallbacks(config); + + let chart = new Chart(config.canvasId, config); + BitChart._bitCharts.set(config.canvasId, chart); + + return true; + } + } + + public static updateChart(config: BitChartConfiguration): boolean { + if (!BitChart._bitCharts.has(config.canvasId)) + throw `Could not find a chart with the given id. ${config.canvasId}`; + + let myChart = BitChart._bitCharts.get(config.canvasId); + + if (!myChart) return false; + + // Update datasets. This breaks the data-array-references; more in the function. + BitChart.mergeDatasets(myChart.config.data!.datasets!, config.data!.datasets!); + // Update labels while keeping array references intact. + BitChart.mergeLabels(myChart.config.data!, config.data!); + // Currently we only merge the datasets and the labels of the data subconfig but that + // could be expanded in a similar fashion as the dataset's data (if there's a use-case). + + BitChart.wireUpCallbacks(config); + + // This will add new options and update existing ones. Nothing is deleted. + // Calling extend instead of merge avoids the unnecessary deep copy as + // config.options is a brand new object (deserialized by blazor). + Chart.helpers.extend(myChart.config.options, config.options); + + myChart.update(); + return true; + } + + private static mergeDatasets(oldDatasets: Array, newDatasets: Array) { + // iterate backwards so we can remove datasets as we go + for (let i = oldDatasets.length - 1; i >= 0; i--) { + let sameDatasetInNewConfig = newDatasets.find(newD => newD.id === oldDatasets[i].id); + if (sameDatasetInNewConfig === undefined) { + // Remove dataset if it's not in the new config + oldDatasets.splice(i, 1); + } else { + // This comment below would be the 'correct' way of updating the data while retaining the same reference. + // However, there's quite a big issue with BitChart. Chart.js actually listenes for modifications on + // the data array and will decide on the update-animation by looking at the latest modifications. + // Since this would clear the whole array and then add all the new data, Chart.js thinks every data + // point is fresh and plays the same animation it plays when initially creating the chart. + // To prevent Chart.js from doing that, we replace the reference which doesn't record any modifications. + + //oldDatasets[i].data.length = 0; // Remove old data + //for (let j = 0; j < sameDatasetInNewConfig.data.length; j++) { + // // Add current data. Of course it won't be a number _and_ a ChartPoint but I don't how else to make ts happy + // oldDatasets[i].data.push(sameDatasetInNewConfig.data[j]); + //} + //delete sameDatasetInNewConfig.data; // Remove the array from the new dataset so it doesn't get copied in the next line + + // Merge everything, including the data-array reference. + // As with the labels, deep copying(with helper.merge) is simply a waste here. + Chart.helpers.extend(oldDatasets[i], sameDatasetInNewConfig); + } + } + + let currentIds = oldDatasets.map(dataset => dataset.id); + newDatasets.filter(newDataset => !currentIds.includes(newDataset.id)) + .forEach(newDataset => oldDatasets.push(newDataset)); + + // Currently the order isn't respected so simply reordering the datasets and calling update + // won't do anything. You'd have to remove and readd them. Maybe this could be implemented later. + } + + private static mergeLabels(oldChartData: Chart.ChartData, newChartData: Chart.ChartData): void { + const innerFunc = (oldLabels: Array, + newLabels: Array) => { + if (newLabels == null || newLabels.length === 0) { + if (oldLabels) { + oldLabels.length = 0; + } + + return oldLabels; + } + + if (oldLabels == null) { + return newLabels; + } + + // clear existing labels + oldLabels.length = 0; + + // add all the new labels + for (let i = 0; i < newLabels.length; i++) { + oldLabels.push(newLabels[i]); + } + + return oldLabels; + } + + // If it was null/undefined before it can't be done in-place so assignment is required. + oldChartData.labels = innerFunc(oldChartData.labels!, newChartData.labels!); + oldChartData.xLabels = innerFunc(oldChartData.xLabels!, newChartData.xLabels!); + oldChartData.yLabels = innerFunc(oldChartData.yLabels!, newChartData.yLabels!); + } + + private static wireUpCallbacks(config: BitChartConfiguration) { + // Replace IMethodHandler objects with actual function (if present) + // This should be "automated" in some way. We shouldn't have to add + // (much) new code for a new callback. + BitChart.wireUpOptionsOnClick(config); + BitChart.wireUpOptionsOnHover(config); + BitChart.wireUpLegendOnClick(config); + BitChart.wireUpLegendOnHover(config); + BitChart.wireUpLegendItemFilter(config); + BitChart.wireUpGenerateLabels(config); + BitChart.wireUpTickCallback(config); + } + + private static wireUpOptionsOnClick(config: BitChartConfiguration) { + let getDefaultFunc = (type: any) => { + let defaults = Chart.defaults[type] || Chart.defaults.global; + return defaults?.onClick || Chart.defaults.global.onClick; + }; + + if (!config.options) return; + + config.options.onClick = BitChart.getMethodHandler(config.options.onClick, getDefaultFunc(config.type)); + } + + private static wireUpOptionsOnHover(config: BitChartConfiguration) { + let getDefaultFunc = (type: any) => { + let defaults = Chart.defaults[type] || Chart.defaults.global; + return defaults?.onHover || Chart.defaults.global.onHover; + }; + + if (!config.options) + return; + + config.options.onHover = BitChart.getMethodHandler(config.options.onHover, getDefaultFunc(config.type)); + } + + private static wireUpLegendOnClick(config: BitChartConfiguration) { + let getDefaultHandler = (type: any) => { + let chartDefaults = Chart.defaults[type] || Chart.defaults.global; + return chartDefaults?.legend?.onClick || Chart.defaults.global.legend?.onClick; + }; + + if (!config.options?.legend) + return; + + config.options.legend.onClick = BitChart.getMethodHandler(config.options.legend.onClick, getDefaultHandler(config.type)); + } + + private static wireUpLegendOnHover(config: BitChartConfiguration) { + let getDefaultFunc = (type: any) => { + let chartDefaults = Chart.defaults[type] || Chart.defaults.global; + return chartDefaults?.legend?.onHover || Chart.defaults.global.legend!.onHover; + }; + + if (!config.options?.legend) + return; + + config.options.legend.onHover = BitChart.getMethodHandler(config.options.legend.onHover, getDefaultFunc(config.type)); + } + + private static wireUpLegendItemFilter(config: BitChartConfiguration) { + let getDefaultFunc = (type: any) => { + let chartDefaults = Chart.defaults[type] || Chart.defaults.global; + return chartDefaults?.legend?.labels?.filter || Chart.defaults.global.legend!.labels!.filter; + }; + + if (!config.options?.legend?.labels) + return; + + config.options.legend.labels.filter = BitChart.getMethodHandler(config.options.legend.labels.filter, getDefaultFunc(config.type)); + } + + private static wireUpGenerateLabels(config: BitChartConfiguration) { + let getDefaultFunc = (type: any) => { + let chartDefaults = Chart.defaults[type] || Chart.defaults.global; + return chartDefaults?.legend?.labels?.generateLabels || Chart.defaults.global.legend!.labels!.generateLabels; + }; + + if (!config.options?.legend?.labels) + return; + + config.options.legend.labels.generateLabels = BitChart.getMethodHandler(config.options.legend.labels.generateLabels, getDefaultFunc(config.type)); + } + + private static wireUpTickCallback(config: BitChartConfiguration) { + /* Defaults table (found out by checking Chart.defaults in console) -> everything undefined + * Bar (scales): undefined + * Bubble (scales): undefined + * Pie & Doughnut: don't even have scale(s) field + * HorizontalBar (scales): undefined + * Line (scales): undefined + * PolarArea (scale): undefined + * Radar (scale): undefined + * Scatter (scales): undefined + */ + + const assignCallbacks = (axes: any) => { + if (axes) { + for (let i = 0; i < axes.length; i++) { + if (!axes[i].ticks) continue; + axes[i].ticks.callback = BitChart.getMethodHandler(axes[i].ticks.callback, undefined); + if (!axes[i].ticks.callback) { + delete axes[i].ticks.callback; // undefined != deleted, Chart.js throws an error if it's undefined so we have to delete it + } + } + } + } + + if (config.options?.scales) { + assignCallbacks(config.options.scales.xAxes); + assignCallbacks(config.options.scales.yAxes); + } + + if (config.options?.scale?.ticks) { + config.options.scale.ticks.callback = BitChart.getMethodHandler(config.options.scale.ticks.callback, undefined); + + if (!config.options.scale.ticks.callback) { + delete config.options.scale.ticks.callback; // undefined != deleted, Chart.js throws an error if it's undefined so we have to delete it + } + } + } + + /** + * Given an IMethodHandler (see C# code), it tries to resolve the referenced method. + * It currently supports JavaScript functions, which are expected to be attached to the window object, and .Net delegates which can be + * bound to .Net static functions, .Net object instance methods and more. + * + * When failing to recover a method from the IMethodHandler, it returns the default handler. + * + * @param handler the serialized IMethodHandler (see C# code) + * @param defaultFunc the fallback value to use in case the method can't be resolved + */ + private static getMethodHandler(handler: IMethodHandler, defaultFunc?: Function) { + if (handler == null) { + return defaultFunc; + } + + if (BitChart.isDelegateHandler(handler)) { + // stringify args and ignore all circular references. This means that objects of type DotNetObject will not be + // deserialized correctly (since it's already a string when it reaches JSON.stringify in the blazor interop layer) + // but the values passed to chart callbacks should never contain such objects anyway. + // Also if we don't care about the value, don't bother to stringify. + const stringifyArgs = (args: any[]) => { + for (let i = 0; i < args.length; i++) { + if (handler.ignoredIndices.includes(i)) { + args[i] = ''; + } else { + args[i] = BitChart.stringifyObjectIgnoreCircular(args[i]); + } + } + + return args; + } + + if (!handler.returnsValue) { + // https://stackoverflow.com/questions/59543973/use-async-function-when-consumer-doesnt-expect-a-promise + return (...args: any[]) => handler.handlerReference.invokeMethodAsync(handler.methodName, stringifyArgs(args)); + } else { + if (window.hasOwnProperty('MONO')) { + return (...args: any[]) => handler.handlerReference.invokeMethod(handler.methodName, stringifyArgs(args)); // only works on client side + } else { + console.warn('Using C# delegates that return values in chart.js callbacks is not supported on ' + + "server side blazor because the server side dispatcher doesn't support synchronous interop calls. Falling back to default value."); + + return defaultFunc; + } + } + } else { + if (handler.methodName == null) { + return defaultFunc; + } + + const namespaceAndFunc: string[] = handler.methodName.split('.'); + if (namespaceAndFunc.length !== 2) { + return defaultFunc; + } + + const namespace = (window as any)[namespaceAndFunc[0]]; + if (namespace == null) { + return defaultFunc; + } + + const func = namespace[namespaceAndFunc[1]]; + if (typeof func === 'function') { + return func; + } else { + return defaultFunc; + } + } + } + + private static isDelegateHandler(handler: IMethodHandler): handler is DelegateHandler { + return 'handlerReference' in handler; + } + + private static stringifyObjectIgnoreCircular(object: any) { + const seen = new WeakSet(); + const replacer = (_name: any, value: any) => { + if ( + typeof value === 'object' && + value !== null && + !(value instanceof Boolean) && + !(value instanceof Date) && + !(value instanceof Number) && + !(value instanceof RegExp) && + !(value instanceof String) + ) { + if (seen.has(value)) + return undefined; + + seen.add(value); + } + + return value; + } + + return JSON.stringify(object, replacer); + } + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BubbleChart/BitChartBubbleConfig.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BubbleChart/BitChartBubbleConfig.cs new file mode 100644 index 0000000000..cb34175fab --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BubbleChart/BitChartBubbleConfig.cs @@ -0,0 +1,12 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents the config for a bubble chart. +/// +public class BitChartBubbleConfig : BitChartConfigBase +{ + /// + /// Creates a new instance of . + /// + public BitChartBubbleConfig() : base(BitChartChartType.Bubble) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BubbleChart/BitChartBubbleDataset.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BubbleChart/BitChartBubbleDataset.cs new file mode 100644 index 0000000000..c18a943889 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BubbleChart/BitChartBubbleDataset.cs @@ -0,0 +1,99 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a dataset for a bubble chart. +/// As per documentation here (Chart.js). +/// +public class BitChartBubbleDataset : BitChartDataset +{ + /// + /// Creates a new instance of . + /// + public BitChartBubbleDataset() : base(BitChartChartType.Bubble) { } + + /// + /// Creates a new instance of with initial data. + /// + public BitChartBubbleDataset(IEnumerable data) : this() + { + AddRange(data); + } + + /// + /// Creates a new instance of with + /// a custom . Use this constructor when + /// you implement a bubble-like chart. + /// + /// The to use instead of . + protected BitChartBubbleDataset(BitChartChartType type) : base(type) { } + + /// + /// Gets or sets the bubble background color. + /// See for working with colors. + /// + public BitChartIndexableOption? BackgroundColor { get; set; } + + /// + /// Gets or sets the bubble border color. + /// See for working with colors. + /// + public BitChartIndexableOption? BorderColor { get; set; } + + /// + /// Gets or sets the bubble border width (in pixels). + /// + public BitChartIndexableOption? BorderWidth { get; set; } + + /// + /// Gets or sets the bubble background color when hovered. + /// See for working with colors. + /// + public BitChartIndexableOption? HoverBackgroundColor { get; set; } + + /// + /// Gets or sets the bubble border color when hovered. + /// See for working with colors. + /// + public BitChartIndexableOption? HoverBorderColor { get; set; } + + /// + /// Gets or sets the bubble border width when hovered (in pixels). + /// + public BitChartIndexableOption? HoverBorderWidth { get; set; } + + /// + /// Gets or sets the bubbles additional radius when hovered (in pixels). + /// + public BitChartIndexableOption? HoverRadius { get; set; } + + /// + /// Gets or sets the bubbles additional radius for hit detection (in pixels). + /// + public BitChartIndexableOption? HitRadius { get; set; } + + /// + /// Gets or sets the label for the dataset which appears in the legend and tooltips. + /// + public string? Label { get; set; } + + /// + /// Gets or sets the drawing order of this dataset. + /// Also affects the order for stacking, tooltips, and the legend. + /// + public int? Order { get; set; } + + /// + /// Gets or sets the for the bubbles in this dataset. + /// + public BitChartIndexableOption? PointStyle { get; set; } + + /// + /// Gets or sets the bubble radius (in pixels). + /// + public BitChartIndexableOption? Radius { get; set; } + + /// + /// Gets or sets the bubble rotation (in degrees). + /// + public BitChartIndexableOption? Rotation { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BubbleChart/BitChartBubbleOptions.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BubbleChart/BitChartBubbleOptions.cs new file mode 100644 index 0000000000..8bed2fe46a --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BubbleChart/BitChartBubbleOptions.cs @@ -0,0 +1,8 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The options-subconfig of a . +/// +public class BitChartBubbleOptions : BitChartBaseConfigOptions +{ +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BubbleChart/BitChartBubblePoint.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BubbleChart/BitChartBubblePoint.cs new file mode 100644 index 0000000000..2db92be874 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/BubbleChart/BitChartBubblePoint.cs @@ -0,0 +1,47 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a bubble on a plane with an X, Y and radius value. +/// +public readonly struct BitChartBubblePoint : IEquatable +{ + /// + /// Gets the X-value of this . + /// + public double X { get; } + + /// + /// Gets the Y-value of this . + /// + public double Y { get; } + + /// + /// Gets the radius of this in pixels. Will be serialized as 'r'. + /// + /// Important: this property is not scaled by the chart, + /// it is the raw radius in pixels of the bubble that is drawn on the canvas. + /// + /// + [Newtonsoft.Json.JsonProperty("r")] + public double Radius { get; } + + /// + /// Creates a new instance of . + /// + /// The X-value for this . + /// The Y-value for this . + /// The radius for this in pixels. + public BitChartBubblePoint(double x, double y, double radius) + { + X = x; + Y = y; + Radius = radius; + } + + public override bool Equals(object? obj) => obj is BitChartBubblePoint point && Equals(point); + public bool Equals(BitChartBubblePoint other) => X == other.X && Y == other.Y && Radius == other.Radius; + public override int GetHashCode() => HashCode.Combine(X, Y, Radius); + + public static bool operator ==(BitChartBubblePoint left, BitChartBubblePoint right) => left.Equals(right); + public static bool operator !=(BitChartBubblePoint left, BitChartBubblePoint right) => !(left == right); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Chartjs.d.ts b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Chartjs.d.ts new file mode 100644 index 0000000000..7935c343f2 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Chartjs.d.ts @@ -0,0 +1,906 @@ +// Type definitions for Chart.js 2.9 +// Modified for use in Bit.BlazorUI.BitChart (removed currently unsupported moment.js references) +// Project: https://github.com/nnnick/Chart.js, https://www.chartjs.org +// Definitions by: Alberto Nuti +// Fabien Lavocat +// KentarouTakeda +// Larry Bahr +// Daniel Luz +// Joseph Page +// Dan Manastireanu +// Guillaume Rodriguez +// Simon Archer +// Ken Elkabany +// Francesco Benedetto +// Alexandros Dorodoulis +// Manuel Heidrich +// Conrad Holtzhausen +// Adrin Caballero +// wertzui +// Martin Trobck +// Elian Cordoba +// Takuya Uehara +// Ricardo Mello +// Ray Nicholus +// Oscar Cabrera +// Carlos Anoceto +// Nobuhiko Futagami +// Marco Ru +// Tony Liu +// Mathias Helminger +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.3 + +declare class Chart { + static readonly Chart: typeof Chart; + constructor( + context: string | CanvasRenderingContext2D | HTMLCanvasElement | ArrayLike, + options: Chart.ChartConfiguration + ); + config: Chart.ChartConfiguration; + data: Chart.ChartData; + destroy: () => {}; + update: ({ duration, lazy, easing }?: Chart.ChartUpdateProps) => {}; + render: ({ duration, lazy, easing }?: Chart.ChartRenderProps) => {}; + stop: () => Chart; + resize: () => Chart; + clear: () => Chart; + toBase64Image: () => string; + generateLegend: () => {}; + getElementAtEvent: (e: any) => [{}]; + getElementsAtEvent: (e: any) => Array<{}>; + getElementsAtXAxis: (e: any) => Array<{}>; + getDatasetAtEvent: (e: any) => Array<{}>; + getDatasetMeta: (index: number) => Meta; + getVisibleDatasetCount: () => number; + isDatasetVisible: (datasetIndex: number) => boolean; + setDatasetVisibility: (datasetIndex: number, visible: boolean) => void; + ctx: CanvasRenderingContext2D | null; + canvas: HTMLCanvasElement | null; + width: number | null; + height: number | null; + aspectRatio: number | null; + options: Chart.ChartOptions; + chartArea: Chart.ChartArea; + static pluginService: PluginServiceStatic; + static plugins: PluginServiceStatic; + + static defaults: { + global: Chart.ChartOptions & Chart.ChartFontOptions & { + tooltips: Chart.ChartTooltipOptions + }; + [key: string]: any; + }; + + static controllers: { + [key: string]: any; + }; + + static helpers: { + merge: (target: any, source: any, options?: { + merger?: Function; + }) => any; + extend: (target: any, ...args: any[]) => any; + [key: string]: any; + }; + + static platform: { + disableCSSInjection: boolean + }; + + static scaleService: { + updateScaleDefaults: (type: Chart.ScaleType, updates: Chart.ChartScales) => void; + }; + + // Tooltip Static Options + static Tooltip: Chart.ChartTooltipsStaticConfiguration; + + static readonly instances: { + [key: string]: Chart; + }; +} +declare class PluginServiceStatic { + register(plugin: Chart.PluginServiceGlobalRegistration & Chart.PluginServiceRegistrationOptions): void; + unregister(plugin: Chart.PluginServiceGlobalRegistration & Chart.PluginServiceRegistrationOptions): void; +} + +interface Meta { + type: Chart.ChartType; + data: MetaData[]; + dataset?: Chart.ChartDataSets; + controller: { [key: string]: any; }; + hidden?: boolean; + total?: string; + xAxisID?: string; + yAxisID?: string; + "$filler"?: { [key: string]: any; }; +} + +interface MetaData { + _chart: Chart; + _datasetIndex: number; + _index: number; + _model: Model; + _start?: any; + _view: Model; + _xScale: Chart.ChartScales; + _yScale: Chart.ChartScales; + hidden?: boolean; +} + +// NOTE: This model is generic with a bunch of optional properties to represent all types of chart models. +// Each chart type defines their own unique model structure so some of these optional properties +// might always have values depending on the chart type. +interface Model { + backgroundColor: string; + borderAlign?: Chart.BorderAlignment; + borderColor: string; + borderWidth?: number; + circumference?: number; + controlPointNextX: number; + controlPointNextY: number; + controlPointPreviousX: number; + controlPointPreviousY: number; + endAngle?: number; + hitRadius: number; + innerRadius?: number; + outerRadius?: number; + pointStyle: string; + radius: string; + skip?: boolean; + startAngle?: number; + steppedLine?: undefined; + tension: number; + x: number; + y: number; + base: number; + head: number; +} + +declare namespace Chart { + type ChartType = 'line' | 'bar' | 'horizontalBar' | 'radar' | 'doughnut' | 'polarArea' | 'bubble' | 'pie' | 'scatter'; + + type TimeUnit = 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'; + + type ScaleType = 'category' | 'linear' | 'logarithmic' | 'time' | 'radialLinear'; + + type PointStyle = 'circle' | 'cross' | 'crossRot' | 'dash' | 'line' | 'rect' | 'rectRounded' | 'rectRot' | 'star' | 'triangle'; + + type PositionType = 'left' | 'right' | 'top' | 'bottom' | 'chartArea'; + + // Allow extending the IteractionMode type alias + // see https://github.com/microsoft/TypeScript/issues/28078#issuecomment-432339564 + interface InteractionModeRegistry { + 'point': 'point'; + 'nearest': 'nearest'; + 'single': 'single'; + 'label': 'label'; + 'index': 'index'; + 'x-axis': 'x-axis'; + 'dataset': 'dataset'; + 'x': 'x'; + 'y': 'y'; + } + type InteractionMode = InteractionModeRegistry[keyof InteractionModeRegistry]; + + type Easing = 'linear' | 'easeInQuad' | 'easeOutQuad' | 'easeInOutQuad' | 'easeInCubic' | 'easeOutCubic' | 'easeInOutCubic' | + 'easeInQuart' | 'easeOutQuart' | 'easeInOutQuart' | 'easeInQuint' | 'easeOutQuint' | 'easeInOutQuint' | 'easeInSine' | 'easeOutSine' | + 'easeInOutSine' | 'easeInExpo' | 'easeOutExpo' | 'easeInOutExpo' | 'easeInCirc' | 'easeOutCirc' | 'easeInOutCirc' | 'easeInElastic' | + 'easeOutElastic' | 'easeInOutElastic' | 'easeInBack' | 'easeOutBack' | 'easeInOutBack' | 'easeInBounce' | 'easeOutBounce' | 'easeInOutBounce'; + + type TextAlignment = 'left' | 'center' | 'right'; + + type BorderAlignment = 'center' | 'inner'; + + type BorderWidth = number | { [key in PositionType]?: number }; + + interface ChartArea { + top: number; + right: number; + bottom: number; + left: number; + } + + interface ChartLegendItem { + text?: string; + fillStyle?: string; + hidden?: boolean; + index?: number; + lineCap?: 'butt' | 'round' | 'square'; + lineDash?: number[]; + lineDashOffset?: number; + lineJoin?: 'bevel' | 'round' | 'miter'; + lineWidth?: number; + strokeStyle?: string; + pointStyle?: PointStyle; + } + + interface ChartLegendLabelItem extends ChartLegendItem { + datasetIndex?: number; + } + + interface ChartTooltipItem { + label?: string; + value?: string; + xLabel?: string | number; + yLabel?: string | number; + datasetIndex?: number; + index?: number; + x?: number; + y?: number; + } + + interface ChartTooltipLabelColor { + borderColor: ChartColor; + backgroundColor: ChartColor; + } + + interface ChartTooltipCallback { + beforeTitle?(item: ChartTooltipItem[], data: ChartData): string | string[]; + title?(item: ChartTooltipItem[], data: ChartData): string | string[]; + afterTitle?(item: ChartTooltipItem[], data: ChartData): string | string[]; + beforeBody?(item: ChartTooltipItem[], data: ChartData): string | string[]; + beforeLabel?(tooltipItem: ChartTooltipItem, data: ChartData): string | string[]; + label?(tooltipItem: ChartTooltipItem, data: ChartData): string | string[]; + labelColor?(tooltipItem: ChartTooltipItem, chart: Chart): ChartTooltipLabelColor; + labelTextColor?(tooltipItem: ChartTooltipItem, chart: Chart): string; + afterLabel?(tooltipItem: ChartTooltipItem, data: ChartData): string | string[]; + afterBody?(item: ChartTooltipItem[], data: ChartData): string | string[]; + beforeFooter?(item: ChartTooltipItem[], data: ChartData): string | string[]; + footer?(item: ChartTooltipItem[], data: ChartData): string | string[]; + afterFooter?(item: ChartTooltipItem[], data: ChartData): string | string[]; + } + + interface ChartAnimationParameter { + chartInstance?: any; + animationObject?: any; + } + + interface ChartPoint { + x?: number | string | Date; + y?: number | string | Date; + r?: number; + t?: number | string | Date; + } + + interface ChartConfiguration { + type?: ChartType | string; + data?: ChartData; + options?: ChartOptions; + plugins?: PluginServiceRegistrationOptions[]; + } + + interface ChartData { + labels?: Array; + xLabels?: Array; + yLabels?: Array; + datasets?: ChartDataSets[]; + } + + interface RadialChartOptions extends ChartOptions { + scale?: RadialLinearScale; + } + + interface ChartSize { + height: number; + width: number; + } + + interface ChartOptions { + responsive?: boolean; + responsiveAnimationDuration?: number; + aspectRatio?: number; + maintainAspectRatio?: boolean; + events?: string[]; + legendCallback?(chart: Chart): string; + onHover?: BitBlazorUI.Legacy.IMethodHandler | ((this: Chart, event: MouseEvent, activeElements: Array<{}>) => any); + onClick?: BitBlazorUI.Legacy.IMethodHandler | ((event?: MouseEvent, activeElements?: Array<{}>) => any); + onResize?(this: Chart, newSize: ChartSize): void; + title?: ChartTitleOptions; + legend?: ChartLegendOptions; + tooltips?: ChartTooltipOptions; + hover?: ChartHoverOptions; + animation?: ChartAnimationOptions; + elements?: ChartElementsOptions; + layout?: ChartLayoutOptions; + scale?: RadialLinearScale; + scales?: ChartScales | LinearScale | LogarithmicScale | TimeScale; + showLines?: boolean; + spanGaps?: boolean; + cutoutPercentage?: number; + circumference?: number; + rotation?: number; + devicePixelRatio?: number; + plugins?: ChartPluginsOptions; + } + + interface ChartFontOptions { + defaultFontColor?: ChartColor; + defaultFontFamily?: string; + defaultFontSize?: number; + defaultFontStyle?: string; + } + + interface ChartTitleOptions { + display?: boolean; + position?: PositionType; + fullWidth?: boolean; + fontSize?: number; + fontFamily?: string; + fontColor?: ChartColor; + fontStyle?: string; + padding?: number; + text?: string | string[]; + } + + interface ChartLegendOptions { + align?: 'center' | 'end' | 'start'; + display?: boolean; + position?: PositionType; + fullWidth?: boolean; + onClick?: BitBlazorUI.Legacy.IMethodHandler | ((event: MouseEvent, legendItem: ChartLegendLabelItem) => void); + onHover?: BitBlazorUI.Legacy.IMethodHandler | ((event: MouseEvent, legendItem: ChartLegendLabelItem) => void); + onLeave?(event: MouseEvent, legendItem: ChartLegendLabelItem): void; + labels?: ChartLegendLabelOptions; + reverse?: boolean; + } + + interface ChartLegendLabelOptions { + boxWidth?: number; + fontSize?: number; + fontStyle?: string; + fontColor?: ChartColor; + fontFamily?: string; + padding?: number; + generateLabels?: BitBlazorUI.Legacy.IMethodHandler | ((chart: Chart) => ChartLegendLabelItem[]); + filter?: BitBlazorUI.Legacy.IMethodHandler | ((legendItem: ChartLegendLabelItem, data: ChartData) => any); + usePointStyle?: boolean; + } + + interface ChartTooltipOptions { + axis?: 'x' | 'y' | 'xy'; + enabled?: boolean; + custom?: (tooltipModel: ChartTooltipModel) => void; + mode?: InteractionMode; + intersect?: boolean; + backgroundColor?: ChartColor; + titleAlign?: TextAlignment; + titleFontFamily?: string; + titleFontSize?: number; + titleFontStyle?: string; + titleFontColor?: ChartColor; + titleSpacing?: number; + titleMarginBottom?: number; + bodyAlign?: TextAlignment; + bodyFontFamily?: string; + bodyFontSize?: number; + bodyFontStyle?: string; + bodyFontColor?: ChartColor; + bodySpacing?: number; + footerAlign?: TextAlignment; + footerFontFamily?: string; + footerFontSize?: number; + footerFontStyle?: string; + footerFontColor?: ChartColor; + footerSpacing?: number; + footerMarginTop?: number; + xPadding?: number; + yPadding?: number; + caretSize?: number; + cornerRadius?: number; + multiKeyBackground?: string; + callbacks?: ChartTooltipCallback; + filter?(item: ChartTooltipItem, data: ChartData): boolean; + itemSort?(itemA: ChartTooltipItem, itemB: ChartTooltipItem, data?: ChartData): number; + position?: string; + caretPadding?: number; + displayColors?: boolean; + borderColor?: ChartColor; + borderWidth?: number; + } + + interface ChartTooltipModel { + afterBody: string[]; + backgroundColor: string; + beforeBody: string[]; + body: ChartTooltipModelBody[]; + bodyFontColor: string; + bodyFontSize: number; + bodySpacing: number; + borderColor: string; + borderWidth: number; + caretPadding: number; + caretSize: number; + caretX: number; + caretY: number; + cornerRadius: number; + dataPoints: ChartTooltipItem[]; + displayColors: boolean; + footer: string[]; + footerFontColor: string; + footerFontSize: number; + footerMarginTop: number; + footerSpacing: number; + height: number; + labelColors: string[]; + labelTextColors: string[]; + legendColorBackground: string; + opacity: number; + title: string[]; + titleFontColor: string; + titleFontSize: number; + titleMarginBottom: number; + titleSpacing: number; + width: number; + x: number; + xAlign: string; + xPadding: number; + y: number; + yAlign: string; + yPadding: number; + _bodyAlign: string; + _bodyFontFamily: string; + _bodyFontStyle: string; + _footerAlign: string; + _footerFontFamily: string; + _footerFontStyle: string; + _titleAlign: string; + _titleFontFamily: string; + _titleFontStyle: string; + } + + interface ChartTooltipModelBody { + before: string[]; + lines: string[]; + after: string[]; + } + + // NOTE: declare plugin options as interface instead of inline '{ [plugin: string]: any }' + // to allow module augmentation in case some plugins want to strictly type their options. + interface ChartPluginsOptions { + [pluginId: string]: any; + } + + interface ChartTooltipsStaticConfiguration { + positioners: { [mode: string]: ChartTooltipPositioner }; + } + + type ChartTooltipPositioner = (elements: any[], eventPosition: Point) => Point; + + interface ChartHoverOptions { + mode?: InteractionMode; + animationDuration?: number; + intersect?: boolean; + axis?: 'x' | 'y' | 'xy'; + onHover?(this: Chart, event: MouseEvent, activeElements: Array<{}>): any; + } + + interface ChartAnimationObject { + currentStep?: number; + numSteps?: number; + easing?: Easing; + render?(arg: any): void; + onAnimationProgress?(arg: any): void; + onAnimationComplete?(arg: any): void; + } + + interface ChartAnimationOptions { + duration?: number; + easing?: Easing; + onProgress?(chart: any): void; + onComplete?(chart: any): void; + animateRotate?: boolean; + animateScale?: boolean; + } + + interface ChartElementsOptions { + point?: ChartPointOptions; + line?: ChartLineOptions; + arc?: ChartArcOptions; + rectangle?: ChartRectangleOptions; + } + + interface ChartArcOptions { + backgroundColor?: ChartColor; + borderColor?: ChartColor; + borderWidth?: number; + } + + interface ChartLineOptions { + cubicInterpolationMode?: 'default' | 'monotone'; + tension?: number; + backgroundColor?: ChartColor; + borderWidth?: number; + borderColor?: ChartColor; + borderCapStyle?: string; + borderDash?: any[]; + borderDashOffset?: number; + borderJoinStyle?: string; + capBezierPoints?: boolean; + fill?: 'zero' | 'top' | 'bottom' | boolean; + stepped?: boolean; + } + + interface ChartPointOptions { + radius?: number; + pointStyle?: PointStyle; + backgroundColor?: ChartColor; + borderWidth?: number; + borderColor?: ChartColor; + hitRadius?: number; + hoverRadius?: number; + hoverBorderWidth?: number; + } + + interface ChartRectangleOptions { + backgroundColor?: ChartColor; + borderWidth?: number; + borderColor?: ChartColor; + borderSkipped?: string; + } + + interface ChartLayoutOptions { + padding?: ChartLayoutPaddingObject | number; + } + + interface ChartLayoutPaddingObject { + top?: number; + right?: number; + bottom?: number; + left?: number; + } + + interface GridLineOptions { + display?: boolean; + circular?: boolean; + color?: ChartColor; + borderDash?: number[]; + borderDashOffset?: number; + lineWidth?: number | number[]; + drawBorder?: boolean; + drawOnChartArea?: boolean; + drawTicks?: boolean; + tickMarkLength?: number; + zeroLineWidth?: number; + zeroLineColor?: ChartColor; + zeroLineBorderDash?: number[]; + zeroLineBorderDashOffset?: number; + offsetGridLines?: boolean; + z?: number; + } + + interface ScaleTitleOptions { + display?: boolean; + labelString?: string; + lineHeight?: number | string; + fontColor?: ChartColor; + fontFamily?: string; + fontSize?: number; + fontStyle?: string; + padding?: ChartLayoutPaddingObject | number; + } + + interface TickOptions extends NestedTickOptions { + minor?: NestedTickOptions | false; + major?: MajorTickOptions | false; + } + + interface NestedTickOptions { + autoSkip?: boolean; + autoSkipPadding?: number; + backdropColor?: ChartColor; + backdropPaddingX?: number; + backdropPaddingY?: number; + beginAtZero?: boolean; + /** + * If the callback returns null or undefined the associated grid line will be hidden. + */ + callback?: BitBlazorUI.Legacy.IMethodHandler | ((value: number | string, index: number, values: number[] | string[]) => string | number | null | undefined); + display?: boolean; + fontColor?: ChartColor; + fontFamily?: string; + fontSize?: number; + fontStyle?: string; + labelOffset?: number; + lineHeight?: number; + max?: any; + maxRotation?: number; + maxTicksLimit?: number; + min?: any; + minRotation?: number; + mirror?: boolean; + padding?: number; + reverse?: boolean; + /** + * The number of ticks to examine when deciding how many labels will fit. + * Setting a smaller value will be faster, but may be less accurate + * when there is large variability in label length. + * Deault: `ticks.length` + */ + sampleSize?: number; + showLabelBackdrop?: boolean; + source?: 'auto' | 'data' | 'labels'; + stepSize?: number; + suggestedMax?: number; + suggestedMin?: number; + } + + interface MajorTickOptions extends NestedTickOptions { + enabled?: boolean; + } + + interface AngleLineOptions { + display?: boolean; + color?: ChartColor; + lineWidth?: number; + borderDash?: number[]; + borderDashOffset?: number; + } + + interface PointLabelOptions { + callback?(arg: any): any; + fontColor?: ChartColor; + fontFamily?: string; + fontSize?: number; + fontStyle?: string; + lineHeight?: number | string; + } + + interface LinearTickOptions extends TickOptions { + maxTicksLimit?: number; + stepSize?: number; + precision?: number; + suggestedMin?: number; + suggestedMax?: number; + } + + // tslint:disable-next-line no-empty-interface + interface LogarithmicTickOptions extends TickOptions { + } + + type ChartColor = string | CanvasGradient | CanvasPattern | string[]; + + type Scriptable = (ctx: { + chart?: Chart; + dataIndex?: number; + dataset?: ChartDataSets + datasetIndex?: number; + }) => T; + + interface ChartDataSets { + id: string; + cubicInterpolationMode?: 'default' | 'monotone'; + backgroundColor?: ChartColor | ChartColor[] | Scriptable; + barPercentage?: number; + barThickness?: number | "flex"; + borderAlign?: BorderAlignment | BorderAlignment[] | Scriptable; + borderWidth?: BorderWidth | BorderWidth[] | Scriptable; + borderColor?: ChartColor | ChartColor[] | Scriptable; + borderCapStyle?: 'butt' | 'round' | 'square'; + borderDash?: number[]; + borderDashOffset?: number; + borderJoinStyle?: 'bevel' | 'round' | 'miter'; + borderSkipped?: PositionType | PositionType[] | Scriptable; + categoryPercentage?: number; + data?: Array | ChartPoint[]; + fill?: boolean | number | string; + hitRadius?: number | number[] | Scriptable; + hoverBackgroundColor?: ChartColor | ChartColor[] | Scriptable; + hoverBorderColor?: ChartColor | ChartColor[] | Scriptable; + hoverBorderWidth?: number | number[] | Scriptable; + hoverRadius?: number; + label?: string; + lineTension?: number; + maxBarThickness?: number; + minBarLength?: number; + steppedLine?: 'before' | 'after' | 'middle' | boolean; + order?: number; + pointBorderColor?: ChartColor | ChartColor[] | Scriptable; + pointBackgroundColor?: ChartColor | ChartColor[] | Scriptable; + pointBorderWidth?: number | number[] | Scriptable; + pointRadius?: number | number[] | Scriptable; + pointRotation?: number | number[] | Scriptable; + pointHoverRadius?: number | number[] | Scriptable; + pointHitRadius?: number | number[] | Scriptable; + pointHoverBackgroundColor?: ChartColor | ChartColor[] | Scriptable; + pointHoverBorderColor?: ChartColor | ChartColor[] | Scriptable; + pointHoverBorderWidth?: number | number[] | Scriptable; + pointStyle?: PointStyle | HTMLImageElement | HTMLCanvasElement | Array | Scriptable; + radius?: number | number[] | Scriptable; + rotation?: number | number[] | Scriptable; + xAxisID?: string; + yAxisID?: string; + type?: ChartType | string; + hidden?: boolean; + hideInLegendAndTooltip?: boolean; + showLine?: boolean; + stack?: string; + spanGaps?: boolean; + weight?: number; + } + + interface ChartScales { + type?: ScaleType | string; + display?: boolean; + position?: PositionType | string; + gridLines?: GridLineOptions; + scaleLabel?: ScaleTitleOptions; + ticks?: TickOptions; + xAxes?: ChartXAxe[]; + yAxes?: ChartYAxe[]; + } + + interface CommonAxe { + bounds?: string; + type?: ScaleType | string; + display?: boolean | string; + id?: string; + stacked?: boolean; + position?: string; + ticks?: TickOptions; + gridLines?: GridLineOptions; + scaleLabel?: ScaleTitleOptions; + time?: TimeScale; + offset?: boolean; + beforeUpdate?(scale?: any): void; + beforeSetDimension?(scale?: any): void; + beforeDataLimits?(scale?: any): void; + beforeBuildTicks?(scale?: any): void; + beforeTickToLabelConversion?(scale?: any): void; + beforeCalculateTickRotation?(scale?: any): void; + beforeFit?(scale?: any): void; + afterUpdate?(scale?: any): void; + afterSetDimension?(scale?: any): void; + afterDataLimits?(scale?: any): void; + afterBuildTicks?(scale: any, ticks: number[]): number[]; + afterTickToLabelConversion?(scale?: any): void; + afterCalculateTickRotation?(scale?: any): void; + afterFit?(scale?: any): void; + } + + interface ChartXAxe extends CommonAxe { + distribution?: 'linear' | 'series'; + } + + // tslint:disable-next-line no-empty-interface + interface ChartYAxe extends CommonAxe { + } + + interface LinearScale extends ChartScales { + ticks?: LinearTickOptions; + } + + interface LogarithmicScale extends ChartScales { + ticks?: LogarithmicTickOptions; + } + + interface TimeDisplayFormat { + millisecond?: string; + second?: string; + minute?: string; + hour?: string; + day?: string; + week?: string; + month?: string; + quarter?: string; + year?: string; + } + + interface DateAdapterOptions { + date?: object; + } + + interface TimeScale extends ChartScales { + adapters?: DateAdapterOptions; + displayFormats?: TimeDisplayFormat; + isoWeekday?: boolean; + max?: string; + min?: string; + parser?: string | ((arg: any) => any); + round?: TimeUnit; + tooltipFormat?: string; + unit?: TimeUnit; + unitStepSize?: number; + stepSize?: number; + minUnit?: TimeUnit; + } + + interface RadialLinearScale { + animate?: boolean; + position?: PositionType; + angleLines?: AngleLineOptions; + pointLabels?: PointLabelOptions; + ticks?: LinearTickOptions; + display?: boolean; + gridLines?: GridLineOptions; + } + + interface Point { + x: number; + y: number; + } + + interface PluginServiceGlobalRegistration { + id?: string; + } + + interface PluginServiceRegistrationOptions { + beforeInit?(chartInstance: Chart, options?: any): void; + afterInit?(chartInstance: Chart, options?: any): void; + + beforeUpdate?(chartInstance: Chart, options?: any): void; + afterUpdate?(chartInstance: Chart, options?: any): void; + + beforeLayout?(chartInstance: Chart, options?: any): void; + afterLayout?(chartInstance: Chart, options?: any): void; + + beforeDatasetsUpdate?(chartInstance: Chart, options?: any): void; + afterDatasetsUpdate?(chartInstance: Chart, options?: any): void; + + beforeDatasetUpdate?(chartInstance: Chart, options?: any): void; + afterDatasetUpdate?(chartInstance: Chart, options?: any): void; + + // This is called at the start of a render. It is only called once, even if the animation will run for a number of frames. Use beforeDraw or afterDraw + // to do something on each animation frame + beforeRender?(chartInstance: Chart, options?: any): void; + afterRender?(chartInstance: Chart, options?: any): void; + + // Easing is for animation + beforeDraw?(chartInstance: Chart, easing: Easing, options?: any): void; + afterDraw?(chartInstance: Chart, easing: Easing, options?: any): void; + + // Before the datasets are drawn but after scales are drawn + beforeDatasetsDraw?(chartInstance: Chart, easing: Easing, options?: any): void; + afterDatasetsDraw?(chartInstance: Chart, easing: Easing, options?: any): void; + + beforeDatasetDraw?(chartInstance: Chart, easing: Easing, options?: any): void; + afterDatasetDraw?(chartInstance: Chart, easing: Easing, options?: any): void; + + // Called before drawing the `tooltip`. If any plugin returns `false`, + // the tooltip drawing is cancelled until another `render` is triggered. + beforeTooltipDraw?(chartInstance: Chart, tooltipData?: any, options?: any): void; + // Called after drawing the `tooltip`. Note that this hook will not, + // be called if the tooltip drawing has been previously cancelled. + afterTooltipDraw?(chartInstance: Chart, tooltipData?: any, options?: any): void; + + // Called when an event occurs on the chart + beforeEvent?(chartInstance: Chart, event: Event, options?: any): void; + afterEvent?(chartInstance: Chart, event: Event, options?: any): void; + + resize?(chartInstance: Chart, newChartSize: ChartSize, options?: any): void; + destroy?(chartInstance: Chart): void; + + /** Deprecated since version 2.5.0. Use `afterLayout` instead. */ + afterScaleUpdate?(chartInstance: Chart, options?: any): void; + } + + interface ChartUpdateProps { + duration?: number; + lazy?: boolean; + easing?: Easing; + } + + interface ChartRenderProps { + duration?: number; + lazy?: boolean; + easing?: Easing; + } + + // Model used with the doughnut chart + interface DoughnutModel { + backgroundColor: ChartColor; + borderAlign: BorderAlignment; + borderColor: string; + borderWidth: number; + circumference: number; + endAngle: number; + innerRadius: number; + outerRadius: number; + startAngle: number; + x: number; + y: number; + } +} + +export = Chart; +export as namespace Chart; diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartAngleLines.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartAngleLines.cs new file mode 100644 index 0000000000..84407d1992 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartAngleLines.cs @@ -0,0 +1,36 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The angle lines sub-config of the linear-radial-axis-configuration (see ). +/// As per documentation here (Chart.js). +/// +public class BitChartAngleLines +{ + /// + /// Gets or sets the value indicating whether the angle line is displayed or not. + /// + public bool? Display { get; set; } + + /// + /// Gets or sets the color of the angled lines. + /// See for working with colors. + /// + public string? Color { get; set; } + + /// + /// Gets or sets the width of the angled lines. + /// + public int? LineWidth { get; set; } + + /// + /// Gets or sets the length and spacing of dashes of the angled lines. + /// As per documentation here (MDN). + /// + public int[]? BorderDash { get; set; } + + /// + /// Gets or sets the offset for line dashes. + /// As per documentation here (MDN). + /// + public double? BorderDashOffset { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartAxis.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartAxis.cs new file mode 100644 index 0000000000..e79eb75f6d --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartAxis.cs @@ -0,0 +1,21 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// As per documentation here (Chart.js). +/// +public abstract class BitChartAxis +{ + /// + /// Controls the axis global visibility (visible when , hidden when ). + /// When display: , the axis is visible only if at least one associated dataset is visible. + /// + public BitChartAxisDisplay? Display { get; set; } + + /// + /// The weight used to sort the axis. Higher weights are further away from the chart area. + /// + public int? Weight { get; set; } + + // TODO: Maybe implement: https://www.chartjs.org/docs/latest/axes/#callbacks + // public object Callbacks { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartCartesianAxis.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartCartesianAxis.cs new file mode 100644 index 0000000000..4c6002ebec --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartCartesianAxis.cs @@ -0,0 +1,56 @@ +using Newtonsoft.Json; + +namespace Bit.BlazorUI.Legacy; + +/// +/// As per documentation here (Chart.js). +/// +public abstract class BitChartCartesianAxis : BitChartAxis +{ + /// + /// The type of axis this instance represents. + /// For js-interop/serialization purposes so Chart.js knows what axis to use. + /// + public abstract BitChartAxisType Type { get; } + + /// + /// The ID is used to link / and together. + /// Referenced in and + /// ( and respectively). + /// + [JsonProperty("id")] + public string? ID { get; set; } + + /// + /// Gets or sets the position of the axis in the chart. + /// + public BitChartPosition? Position { get; set; } + + /// + /// If true, extra space is added to the both edges and the axis is scaled to fit into the chart area. + /// This is set to true for a category scale in a bar chart by default. + /// + public bool? Offset { get; set; } + + /// + /// Gets or sets the scale title configuration. + /// + public BitChartScaleLabel? ScaleLabel { get; set; } + + /// + /// Defines options for the grid lines that run perpendicular to the axis. + /// + public BitChartGridLines? GridLines { get; set; } +} + +/// +/// which contains the ticks-subconfig. +/// +/// +public abstract class BitChartCartesianAxis : BitChartCartesianAxis where TTicks : BitChartCartesianTicks +{ + /// + /// Defines options for the tick marks that are generated by the axis. + /// + public TTicks? Ticks { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartCategoryAxis.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartCategoryAxis.cs new file mode 100644 index 0000000000..2c0e33c2db --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartCategoryAxis.cs @@ -0,0 +1,12 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// This axis is to be used when you want to display values for an axis. +/// This axis has to be used when using/defining , and/or . +/// As per documentation here (Chart.js). +/// +public class BitChartCategoryAxis : BitChartCartesianAxis +{ + /// + public override BitChartAxisType Type => BitChartAxisType.Category; +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartLinearCartesianAxis.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartLinearCartesianAxis.cs new file mode 100644 index 0000000000..11efb9b365 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartLinearCartesianAxis.cs @@ -0,0 +1,12 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The linear scale is use to chart numerical data. It can be placed on either the x or y axis. +/// As the name suggests, linear interpolation is used to determine where a value lies on the axis. +/// As per documentation here (Chart.js). +/// +public class BitChartLinearCartesianAxis : BitChartCartesianAxis +{ + /// + public override BitChartAxisType Type => BitChartAxisType.Linear; +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartLinearRadialAxis.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartLinearRadialAxis.cs new file mode 100644 index 0000000000..7d844971e4 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartLinearRadialAxis.cs @@ -0,0 +1,32 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// A linear radial axis. +/// As per documentation here (Chart.js). +/// +public class BitChartLinearRadialAxis +{ + /// + /// Gets or sets the angle lines configuration. + /// As per documentation here (Chart.js). + /// + public BitChartAngleLines? AngleLines { get; set; } + + /// + /// Gets or sets the grid lines configuration. + /// As per documentation here (Chart.js). + /// + public BitChartGridLines? GridLines { get; set; } + + /// + /// Gets or sets the point labels configuration. + /// As per documentation here (Chart.js). + /// + public BitChartPointLabels? PointLabels { get; set; } + + /// + /// Gets or sets the ticks configuration. + /// As per documentation here (Chart.js). + /// + public BitChartLinearRadialTicks? Ticks { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartLogarithmicAxis.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartLogarithmicAxis.cs new file mode 100644 index 0000000000..1e0e1a06bb --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartLogarithmicAxis.cs @@ -0,0 +1,12 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The logarithmic scale is use to chart numerical data. It can be placed on either the x or y axis. +/// As the name suggests, logarithmic interpolation is used to determine where a value lies on the axis. +/// As per documentation here (Chart.js). +/// +public class BitChartLogarithmicAxis : BitChartCartesianAxis +{ + /// + public override BitChartAxisType Type => BitChartAxisType.Logarithmic; +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartPointLabels.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartPointLabels.cs new file mode 100644 index 0000000000..2d5715bc65 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartPointLabels.cs @@ -0,0 +1,30 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The point labels sub-config of the linear-radial-axis-configuration (see ). +/// As per documentation here (Chart.js). +/// +public class BitChartPointLabels +{ + /// + /// Gets or sets the font color for a point label. + /// See for working with colors. + /// + public BitChartIndexableOption? FontColor { get; set; } + + /// + /// Gets or sets the font size in pixels. + /// + public int? FontSize { get; set; } + + /// + /// Gets or sets the font style to use when rendering a point label. + /// + public BitChartFontStyle? FontStyle { get; set; } + + /// + /// Gets or sets the height of an individual line of text. + /// As per documentation here (MDN). + /// + public double? LineHeight { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartScaleLabel.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartScaleLabel.cs new file mode 100644 index 0000000000..03fe9663fe --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartScaleLabel.cs @@ -0,0 +1,37 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Defines options for how to display an axis title. +/// +public class BitChartScaleLabel +{ + /// + /// Creates a new instance of + /// + /// The initial value for + public BitChartScaleLabel(string? labelString = null) + { + LabelString = labelString; + } + + /// + /// If true, display the axis title. + /// + public bool? Display { get; set; } + + /// + /// Gets or sets the text for the title (i.e. "# of clicks"). + /// + public string? LabelString { get; set; } + + /// + /// Gets or sets the font color of the label. + /// See for working with colors. + /// + public string? FontColor { get; set; } + + /// + /// Gets or sets the font size for scale title. + /// + public int? FontSize { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartTimeAxis.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartTimeAxis.cs new file mode 100644 index 0000000000..41c1681d11 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/BitChartTimeAxis.cs @@ -0,0 +1,27 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The time scale is used to display times and dates. +/// When building its ticks, it will automatically calculate the most comfortable unit base on the size of the scale. +/// As per documentation here (Chart.js). +/// +public class BitChartTimeAxis : BitChartCartesianAxis +{ + /// + public override BitChartAxisType Type => BitChartAxisType.Time; + + /// + /// Gets or sets the distribution which controls the data distribution along the scale. + /// + public BitChartTimeDistribution? Distribution { get; set; } + + /// + /// Gets or sets the bounds which control the scale boundary strategy (bypassed by min/max time options). + /// + public BitChartScaleBound? Bounds { get; set; } + + /// + /// Gets or sets the configuration for time related options. + /// + public BitChartTimeOptions? Time { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartCartesianTicks.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartCartesianTicks.cs new file mode 100644 index 0000000000..ccf00a670f --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartCartesianTicks.cs @@ -0,0 +1,60 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The base class for all tick mark configurations of cartesian axes (see ). Ticks-subconfig of . +/// As per documentation here (Chart.js). +/// +public abstract class BitChartCartesianTicks : BitChartTicks +{ + /// + /// Gets or sets the user defined minimum number for the scale, overrides minimum value from data. + /// + public double? Min { get; set; } + + /// + /// Gets or sets the user defined maximum number for the scale, overrides maximum value from data. + /// + public double? Max { get; set; } + + /// + /// The number of ticks to examine when deciding how many labels will fit. + /// Setting a smaller value will be faster, but may be less accurate when there is large variability in label length. + /// + public int? SampleSize { get; set; } + + /// + /// If true, automatically calculates how many labels can be shown and hides labels accordingly. + /// Labels will be rotated up to maxRotation before skipping any. Turn off to show all labels no matter what. + /// + public bool? AutoSkip { get; set; } + + /// + /// Gets or sets the padding between the ticks on the horizontal axis when is enabled. + /// + public int? AutoSkipPadding { get; set; } + + /// + /// Gets or sets the distance in pixels to offset the label from the centre point of the tick (in the x direction for the x axis, and the y direction for the y axis). + /// Note: this can cause labels at the edges to be cropped by the edge of the canvas. + /// + public int? LabelOffset { get; set; } + + /// + /// Gets or sets the maximum rotation for tick labels when rotating to condense labels. + /// Note: Rotation doesn't occur until necessary. + /// Note: Only applicable to horizontal scales. + /// + public int? MaxRotation { get; set; } + + /// + /// Gets or sets the minimum rotation for tick labels. + /// Note: Only applicable to horizontal scales. + /// + public int? MinRotation { get; set; } + + /// + /// If true, flips tick labels around axis, displaying the labels inside the chart instead of outside. + /// Note: Only applicable to vertical scales. + /// + public bool? Mirror { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartCategoryTicks.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartCategoryTicks.cs new file mode 100644 index 0000000000..a921c116cc --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartCategoryTicks.cs @@ -0,0 +1,24 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The ticks-subconfig of . +/// +public class BitChartCategoryTicks : BitChartCartesianTicks +{ + /// + /// Gets or sets an array of labels to display. + /// + public List? Labels { get; set; } + + /// + /// Gets or sets the minimum item to display. The item has to be present in . + /// Read more . + /// + public new string? Min { get; set; } + + /// + /// Gets or sets the maximum item to display. The item has to be present in . + /// Read more . + /// + public new string? Max { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartLinearCartesianTicks.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartLinearCartesianTicks.cs new file mode 100644 index 0000000000..ab2dbd28c8 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartLinearCartesianTicks.cs @@ -0,0 +1,41 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The ticks-subconfig of . +/// As per documentation here (Chart.js). +/// +public class BitChartLinearCartesianTicks : BitChartCartesianTicks +{ + /// + /// If true, scale will include 0 if it is not already included. + /// + public bool? BeginAtZero { get; set; } + + /// + /// Gets or sets the maximum number of ticks and gridlines to show. + /// + public int? MaxTicksLimit { get; set; } + + /// + /// If defined and is not specified, the step size will be rounded to this many decimal places. + /// + public int? Precision { get; set; } + + /// + /// Gets or sets the user defined fixed step size for the scale. + /// See for details. + /// + public double? StepSize { get; set; } + + /// + /// Gets or sets the adjustment used when calculating the maximum data value. + /// This value is used as the highest value if it's higher than the maximum data-value. + /// + public int? SuggestedMax { get; set; } + + /// + /// Gets or sets the adjustment used when calculating the minimum data value. + /// This value is used as the lowest value if it's lower than the minimum data-value. + /// + public int? SuggestedMin { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartLinearRadialTicks.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartLinearRadialTicks.cs new file mode 100644 index 0000000000..b1c6274a0a --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartLinearRadialTicks.cs @@ -0,0 +1,74 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The ticks sub-config of the . +/// As per documentation here (Chart.js). +/// +public class BitChartLinearRadialTicks : BitChartTicks +{ + /// + /// Gets or sets the color of the label backdrops. + /// See for working with colors. + /// + public string? BackdropColor { get; set; } + + /// + /// Gets or sets the horizontal padding of label backdrop. + /// + public int? BackdropPaddingX { get; set; } + + /// + /// Gets or sets the vertical padding of label backdrop. + /// + public int? BackdropPaddingY { get; set; } + + /// + /// Gets or sets the value indicating whether the scale will include 0 if it is not already included. + /// + public bool? BeginAtZero { get; set; } + + /// + /// Gets or sets the user defined minimum number for the scale, overrides minimum value from data. + /// As per documentation here (Chart.js). + /// + public double? Min { get; set; } + + /// + /// Gets or sets the user defined maximum number for the scale, overrides minimum value from data. + /// As per documentation here (Chart.js). + /// + public double? Max { get; set; } + + /// + /// Gets or sets the maximum number of ticks and gridlines to show. + /// + public int? MaxTicksLimit { get; set; } + + /// + /// If defined and is not specified, the step size will be rounded to this many decimal places. + /// + public int? Precision { get; set; } + + /// + /// Gets or sets the user defined fixed step size for the scale. + /// As per documentation here (Chart.js). + /// + public double? StepSize { get; set; } + + /// + /// Gets or sets the adjustment used when calculating the maximum data value. + /// As per documentation here (Chart.js). + /// + public double? SuggestedMax { get; set; } + + /// + /// Gets or sets the adjustment used when calculating the minimum data value. + /// As per documentation here (Chart.js). + /// + public double? SuggestedMin { get; set; } + + /// + /// Gets or sets the value indicating whether a background is drawn behind the tick labels. + /// + public bool? ShowLabelBackdrop { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartLogarithmicTicks.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartLogarithmicTicks.cs new file mode 100644 index 0000000000..0f794390c5 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartLogarithmicTicks.cs @@ -0,0 +1,9 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The ticks-subconfig of . It has the same members as . +/// As per documentation here (Chart.js). +/// +public class BitChartLogarithmicTicks : BitChartCartesianTicks +{ +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartMajorTicks.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartMajorTicks.cs new file mode 100644 index 0000000000..b9d6f3401d --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartMajorTicks.cs @@ -0,0 +1,13 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The major ticks sub-config of the tick-configuration (see ). +/// As per documentation here (Chart.js). +/// +public class BitChartMajorTicks : BitChartSubTicks +{ + /// + /// Gets or sets the value indicating whether these options are used to show major ticks. + /// + public bool? Enabled { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartMinorTicks.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartMinorTicks.cs new file mode 100644 index 0000000000..dee215992f --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartMinorTicks.cs @@ -0,0 +1,9 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The minor ticks sub-config of the tick-configuration (see ). +/// As per documentation here (Chart.js). +/// +public class BitChartMinorTicks : BitChartSubTicks +{ +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartSubTicks.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartSubTicks.cs new file mode 100644 index 0000000000..981f1b738f --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartSubTicks.cs @@ -0,0 +1,34 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The ultimate base class for tick mark configurations. +/// +public abstract class BitChartSubTicks +{ + /// + /// Gets or sets the font color for a tick's label. + /// See for working with colors. + /// + public string? FontColor { get; set; } + + /// + /// Gets or sets the font family for a tick's label. + /// + public string? FontFamily { get; set; } + + /// + /// Gets or sets the font size for a tick's label. + /// + public int? FontSize { get; set; } + + /// + /// Gets or sets the font style for a tick's label. + /// + public BitChartFontStyle? FontStyle { get; set; } + + /// + /// Gets or sets the height of an individual line of text. + /// As per documentation here (MDN). + /// + public double? LineHeight { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartTicks.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartTicks.cs new file mode 100644 index 0000000000..1f03cc6762 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartTicks.cs @@ -0,0 +1,46 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The base class for all tick mark configurations. Ticks-subconfig of the common . +/// As per documentation here (Chart.js). +/// +public abstract class BitChartTicks : BitChartSubTicks +{ + /// + /// Gets or sets the value indicating whether this axis displays tick marks. + /// + public bool? Display { get; set; } + + /// + /// Gets or sets the value indicating whether the order of the tick labels is reversed. + /// + public bool? Reverse { get; set; } + + /// + /// Gets or sets the minor ticks configuration. Omitted options are inherited. + /// + public BitChartMinorTicks? Minor { get; set; } + + /// + /// Gets or sets the major ticks configuration. Omitted options are inherited. + /// + public BitChartMajorTicks? Major { get; set; } + + /// + /// Gets or sets the offset of the tick labels from the axis. When set on a vertical axis, this applies in the horizontal (X) direction. + /// When set on a horizontal axis, this applies in the vertical (Y) direction. + /// + public int? Padding { get; set; } + + /// + /// Gets or sets the Z-index of the tick layer. Useful when ticks are drawn on chart area. Values <= 0 are drawn under datasets, > 0 on top. + /// + public int? Z { get; set; } + + /// + /// Gets or sets the callback to customize the string representation of the tick value as it should be displayed on the chart. + /// More on . + /// See and . + /// + public IBitChartMethodHandler? Callback { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartTimeTicks.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartTimeTicks.cs new file mode 100644 index 0000000000..6465e5b444 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Axes/Ticks/BitChartTimeTicks.cs @@ -0,0 +1,12 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The ticks-subconfig of a . +/// +public class BitChartTimeTicks : BitChartCartesianTicks +{ + /// + /// Gets or sets how ticks are generated. + /// + public BitChartTickSource? Source { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartAnimation.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartAnimation.cs new file mode 100644 index 0000000000..930285c55d --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartAnimation.cs @@ -0,0 +1,22 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The animation-subconfig of . +/// Specifies options for the animations in this chart. +/// +public class BitChartAnimation +{ + /// + /// Gets or sets the number of milliseconds an animation takes. + /// + public long? Duration { get; set; } + + /// + /// Gets or sets the easing function to use. + /// See for reference. + /// + public BitChartEasing? Easing { get; set; } + + // TODO OnProgress Callback called on each step of an animation. + // TODO OnComplete Callback called at the end of an animation. +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartArcAnimation.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartArcAnimation.cs new file mode 100644 index 0000000000..18f26b40c3 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartArcAnimation.cs @@ -0,0 +1,19 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The animation-subconfig of the options for a radial chart. +/// +public class BitChartArcAnimation : BitChartAnimation +{ + /// + /// Gets or sets a value indicating whether the chart will + /// load in with a rotation animation or not. + /// + public bool? AnimateRotate { get; set; } + + /// + /// Gets or sets a value indicating whether the chart will + /// load in with a scaling animation (from the center outwards) or not. + /// + public bool? AnimateScale { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartBaseConfigOptions.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartBaseConfigOptions.cs new file mode 100644 index 0000000000..cab62794e2 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartBaseConfigOptions.cs @@ -0,0 +1,96 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The base config for the options-subconfig of a chart. +/// +public class BitChartBaseConfigOptions +{ + /// + /// Gets or sets the title of this chart. + /// + public BitChartOptionsTitle? Title { get; set; } + + /// + /// Gets or sets a value indicating whether the chart canvas should be resized when its container is. + /// See important note: here (Chart.js). + /// + public bool? Responsive { get; set; } + + /// + /// Gets or sets the canvas aspect ratio (i.e. width / height, a value of 1 representing a square canvas). + /// Note that this option is ignored if the height is explicitly defined either as attribute (of the canvas) or via the style. + /// + public double? AspectRatio { get; set; } + + /// + /// Gets or sets a value indicating whether to maintain the original canvas aspect ratio (width / height) when resizing. + /// + public bool? MaintainAspectRatio { get; set; } + + /// + /// Gets or sets the duration in milliseconds it takes to animate to new size after a resize event. + /// + public int? ResponsiveAnimationDuration { get; set; } + + /// + /// Gets or sets the legend for this chart. + /// + public BitChartLegend? Legend { get; set; } + + /// + /// Gets or sets the tooltip options for this chart. + /// + public BitChartTooltips? Tooltips { get; set; } + + /// + /// Gets or sets the animation-configuration for this chart. + /// + public BitChartAnimation? Animation { get; set; } + + /// + /// Gets the plugin options. The key has to be the unique + /// identification of the plugin. + /// + /// Reference for Chart.js plugin options: + /// + /// + /// + public Dictionary Plugins { get; } = []; + + /// + /// Gets or sets the browser events that the chart should listen to for tooltips and hovering. + /// + /// If , this includes , , + /// , and (by default). + /// + /// + public BitChartBrowserEvent[]? Events { get; set; } + + /// + /// Gets or sets the callback to call when an event of type or + /// fires on the chart. + /// Called in the context of the chart and passed the event and an array of active elements. + /// See and . + /// + public IBitChartMethodHandler? OnClick { get; set; } + + /// + /// Gets or sets the callback to call when any of the fire on the chart. + /// Called in the context of the chart and passed the event and an array of + /// active elements (bars, points, etc). + /// See and . + /// + public IBitChartMethodHandler? OnHover { get; set; } + + /// + /// Gets or sets the hover configuration for this chart. + /// + public BitChartHover? Hover { get; set; } + + /// + /// This method tells json.net to only serialize the plugin options when + /// there are plugin options, don't call it directly. + /// + [Obsolete("Only for json.net, don't call it.", true)] + public bool ShouldSerializePlugins() => Plugins.Count > 0; +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartChartData.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartChartData.cs new file mode 100644 index 0000000000..a70e8b892d --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartChartData.cs @@ -0,0 +1,57 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents the data-subconfig of any . +/// +public class BitChartChartData +{ + /// + /// Creates a new instance of . + /// + public BitChartChartData() + { + Labels = []; + XLabels = []; + YLabels = []; + Datasets = []; + } + + /// + /// Gets the labels the chart will use. + /// + /// If defined (1 or more labels) the corresponding axis has to be of type + /// for the chart to work correctly. + /// + /// + public virtual List Labels { get; } + + /// + /// Gets the labels the horizontal axes will use. + /// + /// If defined (1 or more labels) the x-axis has to be of type + /// for the chart to work correctly. + /// + /// + public virtual List XLabels { get; } + + /// + /// Gets the labels the vertical axes will use. + /// + /// If defined (1 or more labels) the y-axis has to be of type + /// for the chart to work correctly. + /// + /// + public virtual List YLabels { get; } + + /// + /// Gets the datasets displayed in this chart. + /// + public List Datasets { get; } + + [Obsolete("json.net", true)] + public bool ShouldSerializeLabels() => Labels?.Count > 0; + [Obsolete("json.net", true)] + public bool ShouldSerializeXLabels() => XLabels?.Count > 0; + [Obsolete("json.net", true)] + public bool ShouldSerializeYLabels() => YLabels?.Count > 0; +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartClipping.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartClipping.cs new file mode 100644 index 0000000000..45ccc89400 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartClipping.cs @@ -0,0 +1,190 @@ +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; + +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents how lines are clipped relative to the chart area. +/// As per documentation here (Chart.js). +/// For any given edge: +/// +/// 0 means clipping at the chart area. +/// negative values mean clipping inside the chart area. +/// positive values mean clipping outside the chart area. +/// means no clipping. +/// +/// +/// +[JsonConverter(typeof(ClippingJsonConverter))] +public readonly struct BitChartClipping : IEquatable +{ + internal readonly bool _equalSides; + + /// + /// Gets the clipping for the top edge. + /// + public int? Top { get; } + + /// + /// Gets the clipping for the right edge. + /// + public int? Right { get; } + + /// + /// Gets the clipping for the bottom edge. + /// + public int? Bottom { get; } + + /// + /// Gets the clipping for the left edge. + /// + public int? Left { get; } + + /// + /// Creates a new instance of + /// using the supplied value for all edges. + /// + /// The clipping value for all edges. + public BitChartClipping(int all) + { + Top = Right = Bottom = Left = all; + _equalSides = true; + } + + /// + /// Creates a new instance of + /// using individual values for all edges. + /// + /// The clipping value for the bottom edge. + /// The clipping value for the left edge. + /// The clipping value for the top edge. + /// The clipping value for the right edge. + public BitChartClipping(int? top = null, int? right = null, int? bottom = null, int? left = null) + { + Top = top; + Right = right; + Bottom = bottom; + Left = left; + _equalSides = false; + } + + /// + /// Converts an value to a implicitly. + /// The supplied value will be used for all edges. + /// + /// The clipping value for all edges. + public static implicit operator BitChartClipping(int value) => new BitChartClipping(value); + + /// + public override string ToString() + { + return $"{nameof(Top)}: {ValOrNull(Top)}, " + + $"{nameof(Right)}: {ValOrNull(Right)}, " + + $"{nameof(Bottom)}: {ValOrNull(Bottom)}, " + + $"{nameof(Left)}: {ValOrNull(Left)}"; + + static string ValOrNull(int? value) => value.HasValue ? value.Value.ToString() : "null"; + } + + public override bool Equals(object? obj) => obj is BitChartClipping clipping && Equals(clipping); + + public bool Equals(BitChartClipping other) + { + if (_equalSides && other._equalSides) return Top == other.Top; + + return Top == other.Top && + Right == other.Right && + Bottom == other.Bottom && + Left == other.Left; + } + + public override int GetHashCode() => HashCode.Combine(Bottom, Left, Top, Right); + + public static bool operator ==(BitChartClipping left, BitChartClipping right) => left.Equals(right); + public static bool operator !=(BitChartClipping left, BitChartClipping right) => !(left == right); +} + +internal class ClippingJsonConverter : JsonConverter +{ + public override BitChartClipping ReadJson(JsonReader reader, Type objectType, [AllowNull] BitChartClipping existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Integer) + return new BitChartClipping((int?)(long?)reader.Value); + + if (reader.TokenType != JsonToken.StartObject) + throw new JsonReaderException(); + + JObject obj = JObject.Load(reader); + int? top = GetClippingValue(obj, nameof(BitChartClipping.Top)); + int? right = GetClippingValue(obj, nameof(BitChartClipping.Right)); + int? bottom = GetClippingValue(obj, nameof(BitChartClipping.Bottom)); + int? left = GetClippingValue(obj, nameof(BitChartClipping.Left)); + + return new BitChartClipping(top, right, bottom, left); + } + + private static int? GetClippingValue(JObject obj, string name) + { + if (!obj.TryGetValue(name, StringComparison.OrdinalIgnoreCase, out JToken? token)) + return null; + + if (token.Type == JTokenType.Boolean && (bool)token is false) + return null; + + if (token.Type == JTokenType.Integer) + return (int)token; + + throw new JsonWriterException(); + } + + public override void WriteJson(JsonWriter writer, [AllowNull] BitChartClipping value, JsonSerializer serializer) + { + if (value._equalSides) + { + writer.WriteValue(value.Bottom!.Value); + return; + } + + var naming = (serializer.ContractResolver as DefaultContractResolver)?.NamingStrategy; + + writer.WriteStartObject(); + + WriteAdjustedName(writer, naming, nameof(BitChartClipping.Top)); + WriteValueOrFalse(writer, value.Top); + + WriteAdjustedName(writer, naming, nameof(BitChartClipping.Right)); + WriteValueOrFalse(writer, value.Right); + + WriteAdjustedName(writer, naming, nameof(BitChartClipping.Bottom)); + WriteValueOrFalse(writer, value.Bottom); + + WriteAdjustedName(writer, naming, nameof(BitChartClipping.Left)); + WriteValueOrFalse(writer, value.Left); + + writer.WriteEndObject(); + } + + private static void WriteAdjustedName(JsonWriter writer, NamingStrategy? namingStrategy, string name) + { + if (namingStrategy != null) + { + name = namingStrategy.GetPropertyName(name, false); + } + + writer.WritePropertyName(name); + } + + private static void WriteValueOrFalse(JsonWriter writer, int? value) + { + if (value.HasValue) + { + writer.WriteValue(value.Value); + } + else + { + writer.WriteValue(false); + } + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartConfigBase.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartConfigBase.cs new file mode 100644 index 0000000000..4c16178c22 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartConfigBase.cs @@ -0,0 +1,89 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Base class for chart-configs +/// Contains the most basic required information about a chart. +/// +public abstract class BitChartConfigBase +{ + /// + /// Creates a new instance of . + /// + /// The this config is for. + protected BitChartConfigBase(BitChartChartType chartType) + { + Type = chartType; + } + + /// + /// Gets the type of chart this config is for. + /// + public BitChartChartType Type { get; } + + /// + /// Gets the id for the html canvas element associated with this chart. + /// This property is initialized to a random GUID-string upon creation. + /// + public string CanvasId { get; } = Guid.NewGuid().ToString(); + + /// + /// Gets the list of inline plugins for this chart. Consider + /// registering global plugins (through JavaScript) or assigning the + /// plugins through JavaScript instead of using this property + /// since these plugins work mostly with method hooks and methods + /// can't be serialized. It could be supported, but just isn't yet. + /// + /// Reference for Chart.js inline plugins: + /// . + /// + /// + /// For configuring plugins (plugin options), you need to use + /// instead. + /// + /// + public List Plugins { get; } = new List(); + + /// + /// This method tells json.net to only serialize the plugins when there + /// are plugins, don't call it directly. + /// + [Obsolete("Only for json.net, don't call it.", true)] + public bool ShouldSerializePlugins() => Plugins.Count > 0; +} + +/// +/// Base class for chart-configs which contains the options and the data subconfigs. +/// +/// The type of the options subconfig. +/// The type of the data subconfig. +public abstract class BitChartConfigBase : BitChartConfigBase + where TOptions : BitChartBaseConfigOptions + where TData : BitChartChartData, new() +{ + /// + /// Creates a new instance of . + /// + /// The this config is for. + protected BitChartConfigBase(BitChartChartType chartType) : base(chartType) + { + Data = new TData(); + } + + /// + /// Gets or sets the options subconfig for this chart. + /// + public TOptions? Options { get; set; } + + /// + /// Gets the data subconfig for this chart. + /// + public TData Data { get; } +} + +/// +public abstract class BitChartConfigBase : BitChartConfigBase + where TOptions : BitChartBaseConfigOptions +{ + /// + protected BitChartConfigBase(BitChartChartType chartType) : base(chartType) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartDataset.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartDataset.cs new file mode 100644 index 0000000000..1940f055d2 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartDataset.cs @@ -0,0 +1,80 @@ +using System.Collections.ObjectModel; +using Newtonsoft.Json; + +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a dataset containing a collection of values. +/// +/// The type of data this contains. +[JsonObject] +public abstract class BitChartDataset : Collection, IDataset +{ + /// + /// Gets the ID of this dataset. Used to keep track of the datasets + /// across the .NET <-> JavaScript boundary. + /// + public string Id { get; } + + /// + /// Gets the data contained in this dataset. This property is read-only. + /// This is in addition to implementing . + /// + public IReadOnlyList Data { get; } + + /// + /// Gets the this dataset is for. + /// Important to set in mixed charts. + /// + public BitChartChartType Type { get; } + + /// + /// Creates a new . + /// + /// The this dataset is for. + /// The id for this dataset. If , + /// a random GUID string will be used. + protected BitChartDataset(BitChartChartType type, string? id = null) : base([]) + { + Data = new ReadOnlyCollection(Items); + Id = id ?? Guid.NewGuid().ToString(); + Type = type; + } + + /// + /// Adds the elements of the specified collection to the end of the . + /// + /// + /// The collection whose elements should be added to the end of the . + /// + public void AddRange(IEnumerable items) => ((List)Items).AddRange(items ?? throw new ArgumentNullException(nameof(items))); + + /// + /// Adds the elements of the specified collection to the end of the . + /// + /// + /// The collection whose elements should be added to the end of the . + /// + public void AddRange(params T[] items) => AddRange(items as IEnumerable); + + /// + /// Determines whether the specified object is considered equal to the current object. + /// + /// The object to compare with the current object. + /// if the specified object is considered to be equal + /// to the current object; otherwise, . + public override bool Equals(object? obj) => obj is BitChartDataset set && + Id == set.Id && + EqualityComparer>.Default.Equals(Items, set.Items); + + /// + /// Returns the hash code for this . + /// + /// The hash code for this . + public override int GetHashCode() => HashCode.Combine(Items, Id); + + public static bool operator ==(BitChartDataset left, BitChartDataset right) => + EqualityComparer>.Default.Equals(left, right); + + public static bool operator !=(BitChartDataset left, BitChartDataset right) => !(left == right); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartGridLines.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartGridLines.cs new file mode 100644 index 0000000000..137c7fc1ee --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartGridLines.cs @@ -0,0 +1,82 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The grid lines sub-config of the axis-configuration (see ). +/// +public class BitChartGridLines +{ + /// + /// If false, do not display grid lines for this axis. + /// + public bool? Display { get; set; } + + /// + /// If true, gridlines are circular (on radar chart only). + /// + public bool? Circular { get; set; } + + /// + /// The color of the grid lines. If specified as an array, the first color applies to the first grid line, the second to the second grid line and so on. + /// See for working with colors. + /// + public BitChartIndexableOption? Color { get; set; } + + /// + /// Length and spacing of dashes on grid lines + /// See for patterns and details + /// + public double[]? BorderDash { get; set; } + + /// + /// Stroke width of grid lines. + /// + public BitChartIndexableOption? LineWidth { get; set; } + + /// + /// If true, draw border at the edge between the axis and the chart area. + /// + public bool? DrawBorder { get; set; } + + /// + /// If true, draw lines on the chart area inside the axis lines. This is useful when there are multiple axes and you need to control which grid lines are drawn. + /// + public bool? DrawOnChartArea { get; set; } + + /// + /// If true, draw lines beside the ticks in the axis area beside the chart. + /// + public bool? DrawTicks { get; set; } + + /// + /// Length in pixels that the grid lines will draw into the axis area. + /// + public int? TickMarkLength { get; set; } + + /// + /// Stroke width of the grid line for the first index (index 0). + /// + public int? ZeroLineWidth { get; set; } + + /// + /// Stroke color of the grid line for the first index (index 0). + /// See for working with colors. + /// + public string? ZeroLineColor { get; set; } + + /// + /// Length and spacing of dashes of the grid line for the first index (index 0). + /// See for details. + /// + public double[]? ZeroLineBorderDash { get; set; } + + /// + /// Offset for line dashes of the grid line for the first index (index 0). + /// See for details. + /// + public double? ZeroLineBorderDashOffset { get; set; } + + /// + /// If true, grid lines will be shifted to be between labels. This is set to true for a category scale in a bar chart by default. + /// + public bool? OffsetGridLines { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartHover.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartHover.cs new file mode 100644 index 0000000000..e2648b4afb --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartHover.cs @@ -0,0 +1,30 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The hover configuration contains general settings regarding hovering over a chart. +/// +public class BitChartHover +{ + /// + /// Gets or sets which elements appear in the tooltip. + /// + public BitChartInteractionMode? Mode { get; set; } + + /// + /// Gets or sets a value indicating whether or not the hover mode only applies + /// when the mouse position intersects an item on the chart. + /// + public bool? Intersect { get; set; } + + /// + /// Gets or sets which directions are used in calculating distances. + /// Defaults to for == + /// and for == or . + /// + public BitChartAxisDirection? Axis { get; set; } + + /// + /// Gets or sets the duration in milliseconds it takes to animate hover style changes. + /// + public long? AnimationDuration { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartIndexableOption.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartIndexableOption.cs new file mode 100644 index 0000000000..c4411d2fcc --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartIndexableOption.cs @@ -0,0 +1,184 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents an object that can be either a single value or an array of values. This is used for type safe js-interop. +/// +/// The type of data this is supposed to hold. +[Newtonsoft.Json.JsonConverter(typeof(IndexableOptionConverter))] // newtonsoft for now +public class BitChartIndexableOption : IEquatable> +{ + /// + /// The compile-time name of the property which gets the wrapped value. This is used internally for serialization. + /// + internal const string PropertyName = nameof(BoxedValue); + + // for serialization, there has to be a cast to object anyway + internal object? BoxedValue => IsIndexed ? IndexedValues : SingleValue; + + private readonly T? _singleValue; + private readonly T[]? _indexedValues; + + /// + /// The indexed values represented by this instance. + /// + public T[]? IndexedValues + { + get + { + if (!IsIndexed) + throw new InvalidOperationException("This instance represents a single value. The indexed values are not available."); + + return _indexedValues; + } + } + + /// + /// The single value represented by this instance. + /// + public T? SingleValue + { + get + { + if (IsIndexed) + throw new InvalidOperationException("This instance represents an array of values. The single value is not available."); + + return _singleValue; + } + } + + /// + /// Gets the value indicating whether the option wrapped in this is indexed. + /// True if the wrapped value represents an array of , false if it represents a single value of . + /// + public bool IsIndexed { get; } + + /// + /// Creates a new instance of which represents a single value. + /// + /// The single value this should represent. + public BitChartIndexableOption(T? singleValue) + { + _singleValue = singleValue != null ? singleValue : throw new ArgumentNullException(nameof(singleValue)); + IsIndexed = false; + } + + /// + /// Creates a new instance of which represents an array of values. + /// + /// The array of values this should represent. + public BitChartIndexableOption(T[]? indexedValues) + { + _indexedValues = indexedValues ?? throw new ArgumentNullException(nameof(indexedValues)); + IsIndexed = true; + } + + /// + /// Implicitly wraps a single value of to a new instance of . + /// + /// The single value to wrap + public static implicit operator BitChartIndexableOption(T? singleValue) + { + CheckIsNotIndexableOption(singleValue?.GetType()); + + return new BitChartIndexableOption(singleValue); + } + + /// + /// Implicitly wraps an array of values of to a new instance of . + /// + /// The array of values to wrap + public static implicit operator BitChartIndexableOption(T[]? indexedValues) + { + CheckIsNotIndexableOption(indexedValues?.GetType().GetElementType()); + + return new BitChartIndexableOption(indexedValues); + } + + private static void CheckIsNotIndexableOption(Type? type) + { + ArgumentNullException.ThrowIfNull(type); + + if (!type.IsGenericType) return; + if (type.GetGenericTypeDefinition() == typeof(BitChartIndexableOption<>)) + throw new ArgumentException("You cannot use an indexable option inside an indexable option."); + } + + /// + /// Determines whether the specified instance is considered equal to the current instance. + /// + /// The to compare with. + /// true if the objects are considered equal; otherwise, false. + public bool Equals(BitChartIndexableOption? other) + { + if (IsIndexed != other?.IsIndexed) return false; + + if (IsIndexed) + { + if (IndexedValues == other?.IndexedValues) return true; + + if (IndexedValues is null || other is null || other.IndexedValues is null) return false; + + return Enumerable.SequenceEqual(IndexedValues, other.IndexedValues); + } + else + { + return EqualityComparer.Default.Equals(SingleValue, other.SingleValue); + } + } + + /// + /// Determines whether the specified object instance is considered equal to the current instance. + /// + /// The object to compare with. + /// true if the objects are considered equal; otherwise, false. + public override bool Equals(object? obj) + { + // an indexable option cannot store null + if (obj == null) return false; + + if (obj is BitChartIndexableOption option) + { + return Equals(option); + } + else + { + if (IsIndexed) + { + return IndexedValues?.Equals(obj) ?? false; + } + else + { + return SingleValue?.Equals(obj) ?? false; + } + } + } + + /// + /// Returns the hash of the underlying object. + /// + /// The hash of the underlying object. + public override int GetHashCode() + { + var hashCode = -506568782; + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(_indexedValues!); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(_singleValue!); + hashCode = hashCode * -1521134295 + IsIndexed.GetHashCode(); + return hashCode; + } + + /// + /// Determines whether two specified instances contain the same value. + /// + /// The first to compare + /// The second to compare + /// true if the value of a is the same as the value of b; otherwise, false. + public static bool operator ==(BitChartIndexableOption a, BitChartIndexableOption b) => a.Equals(b); + + /// + /// Determines whether two specified instances contain different values. + /// + /// The first to compare + /// The second to compare + /// true if the value of a is different from the value of b; otherwise, false. + public static bool operator !=(BitChartIndexableOption a, BitChartIndexableOption b) => !(a == b); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartLegend.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartLegend.cs new file mode 100644 index 0000000000..0ab75e78ee --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartLegend.cs @@ -0,0 +1,46 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The chart legend displays data about the datasets that are appearing on the chart. +/// As per documentation here (Chart.js). +/// +public class BitChartLegend +{ + /// + /// Determines if the legend is displayed + /// + public bool? Display { get; set; } + + /// + /// Position of the legend + /// + public BitChartPosition? Position { get; set; } + + /// + /// Marks that this box should take the full width of the canvas (pushing down other boxes). + /// This is unlikely to need to be changed in day-to-day use. + /// + public bool? FullWidth { get; set; } + + /// + /// Gets or sets the callback to call when a click event is registered on a label item. + /// See and . + /// + public IBitChartMethodHandler? OnClick { get; set; } + + /// + /// Gets or sets the callback to call when a event is registered on top of a label item. + /// See and . + /// + public IBitChartMethodHandler? OnHover { get; set; } + + /// + /// Legend will show datasets in reverse order. + /// + public bool? Reverse { get; set; } + + /// + /// Configuration options for the legend-labels + /// + public BitChartLegendLabels? Labels { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartLegendItem.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartLegendItem.cs new file mode 100644 index 0000000000..1a144222c2 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartLegendItem.cs @@ -0,0 +1,93 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The model of the legend items which are displayed in the chart-legend. +/// +/// As per documentation here: +/// +/// +/// +public class BitChartLegendItem +{ + /// + /// Gets or sets the index of the dataset this legend item corresponds to. + /// DO NOT set this value when returning an instance of this class to Chart.js. + /// Only use this property when retrieving the index in a legend-event. + /// This value might not be set in charts that have legend labels per value + /// (instead of per dataset) like pie-chart. + /// + public int? DatasetIndex { get; set; } + + /// + /// Gets or sets the index of the value this legend item corresponds to. + /// DO NOT set this value when returning an instance of this class to Chart.js. + /// Only use this property when retrieving the index in a legend-event. + /// This value is only set in charts that have legend labels per value + /// (instead of per dataset) like pie-chart. + /// + public int? Index { get; set; } + + /// + /// Gets or sets the label-text that will be displayed. + /// + public string? Text { get; set; } + + /// + /// Gets or sets the color (style) of the legend box. + /// See for working with colors. + /// + public string? FillStyle { get; set; } + + /// + /// Gets or sets a value indicating whether or not this legend-item represents a + /// hidden dataset. Label will be rendered with a strike-through effect if . + /// + public bool? Hidden { get; set; } + + /// + /// Gets or sets the for the legend box border. + /// + public BitChartBorderCapStyle? LineCap { get; set; } + + /// + /// Gets or sets the line dash segments for the legend box border. + /// Details on + /// . + /// + public double[]? LineDash { get; set; } + + /// + /// Gets or sets the line dash offset. + /// Details on + /// . + /// + public double? LineDashOffset { get; set; } + + /// + /// Gets or sets the of the legend box border. + /// + public BitChartBorderJoinStyle? LineJoin { get; set; } + + /// + /// Gets or sets the width of the box border. + /// + public double? LineWidth { get; set; } + + /// + /// Gets or sets the color (style) of the legend box border. + /// See for working with colors. + /// + public string? StrokeStyle { get; set; } + + /// + /// Gets or sets the of the legend box + /// (only used if is ). + /// + public BitChartPointStyle? PointStyle { get; set; } + + /// + /// Gets or sets the rotation of the point in degrees + /// (only used if is ). + /// + public double? Rotation { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartLegendLabels.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartLegendLabels.cs new file mode 100644 index 0000000000..dda8728d56 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartLegendLabels.cs @@ -0,0 +1,53 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The legend label configuration is nested below the legend configuration +/// +public class BitChartLegendLabels +{ + /// + /// Gets or sets the width of the colored box. + /// + public int? BoxWidth { get; set; } + + /// + /// Gets or sets the font size for the labels text. + /// + public int? FontSize { get; set; } + + /// + /// Gets or sets the font style for the labels text. + /// + public BitChartFontStyle? FontStyle { get; set; } + + /// + /// Gets or sets the color of the text. + /// See for working with colors. + /// + public string? FontColor { get; set; } + + /// + /// Gets or sets the padding between rows of colored boxes. + /// + public int? Padding { get; set; } + + /// + /// Gets or sets the callback to generate legend items for a chart. + /// Default implementation returns the text + styling for the color box. + /// See and . + /// + public IBitChartMethodHandler? GenerateLabels { get; set; } + + /// + /// Gets or sets the callback to filter legend items out of the legend. + /// Receives 2 parameters, a and the chart data. The chart data large so + /// consider applying a if you don't use the value. + /// See and . + /// + public IBitChartMethodHandler? Filter { get; set; } + + /// + /// Label style will match corresponding point style (size is based on , is not used in this case). + /// + public bool? UsePointStyle { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartOptionsTitle.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartOptionsTitle.cs new file mode 100644 index 0000000000..f78e61b352 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartOptionsTitle.cs @@ -0,0 +1,54 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The title-subconfig of . Specifies how the chart title is displayed. +/// +public class BitChartOptionsTitle +{ + /// + /// Gets or sets a value indicating whether the title should be displayed or not. + /// + public bool? Display { get; set; } + + /// + /// Gets or sets the position of the title. + /// + public BitChartPosition? Position { get; set; } + + /// + /// Gets or sets the font size for the title text. + /// + public int? FontSize { get; set; } + + /// + /// Gets or sets the font family for the title text. + /// + public string? FontFamily { get; set; } + + /// + /// Gets or sets the font color for the title text. + /// See for working with colors. + /// + public string? FontColor { get; set; } + + /// + /// Gets or sets the font style for the title text. + /// + public BitChartFontStyle? FontStyle { get; set; } + + /// + /// Gets or sets the number of pixels to add above and below the title text. + /// + public int? Padding { get; set; } + + /// + /// Gets or sets the height of an individual line of text. + /// As per documentation here (MDN). + /// + public double? LineHeight { get; set; } + + /// + /// Gets or sets the title text to display. If specified as an array, text is rendered on multiple lines. + /// + public BitChartIndexableOption? Text { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartPoint.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartPoint.cs new file mode 100644 index 0000000000..090379273d --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartPoint.cs @@ -0,0 +1,35 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a point on a plane with an X and a Y-value. +/// +public readonly struct BitChartPoint : IEquatable +{ + /// + /// Gets the X-value of this . + /// + public double X { get; } + + /// + /// Gets the Y-value of this . + /// + public double Y { get; } + + /// + /// Creates a new instance of . + /// + /// The X-value for this . + /// The Y-value for this . + public BitChartPoint(double x, double y) + { + X = x; + Y = y; + } + + public override bool Equals(object? obj) => obj is BitChartPoint point && Equals(point); + public bool Equals(BitChartPoint other) => X == other.X && Y == other.Y; + public override int GetHashCode() => HashCode.Combine(X, Y); + + public static bool operator ==(BitChartPoint left, BitChartPoint right) => left.Equals(right); + public static bool operator !=(BitChartPoint left, BitChartPoint right) => !(left == right); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartTooltips.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartTooltips.cs new file mode 100644 index 0000000000..ea31f9aa34 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/BitChartTooltips.cs @@ -0,0 +1,169 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The tooltips-subconfig of the common options (applies to all charts). +/// As per documentation here (Chart.js). +/// +public class BitChartTooltips +{ + /// + /// Gets or sets which elements appear in the tooltip. + /// + public BitChartInteractionMode? Mode { get; set; } + + /// + /// Gets or sets the value indicating if the hover mode only applies when the mouse position intersects an item on the chart. + /// + public bool? Intersect { get; set; } + + /// + /// Gets or sets the value indicating if the tooltips are enabled. + /// + public bool? Enabled { get; set; } + + /// + /// Gets or sets the mode for positioning the tooltip. + /// + public BitChartTooltipPosition? Position { get; set; } + + /// + /// Gets or sets the background color of the tooltip. + /// See for working with colors. + /// + public string? BackgroundColor { get; set; } + + /// + /// Gets or sets the title font. + /// + public string? TitleFontFamily { get; set; } + + /// + /// Gets or sets the title font size. + /// + public double? TitleFontSize { get; set; } + + /// + /// Gets or sets the title font style. + /// + public BitChartFontStyle? TitleFontStyle { get; set; } + + /// + /// Gets or sets the title font color. + /// See for working with colors. + /// + public string? TitleFontColor { get; set; } + + /// + /// Gets or sets the spacing to add to top and bottom of each title line. + /// + public double? TitleSpacing { get; set; } + + /// + /// Gets or sets the spacing to add to top and bottom of each title line. + /// + public double? TitleMarginBottom { get; set; } + + /// + /// Gets or sets the body line font. + /// + public string? BodyFontFamily { get; set; } + + /// + /// Gets or sets the body font size. + /// + public double? BodyFontSize { get; set; } + + /// + /// Gets or sets the body font style. + /// + public BitChartFontStyle? BodyFontStyle { get; set; } + + /// + /// Gets or sets the body font color. + /// See for working with colors. + /// + public string? BodyFontColor { get; set; } + + /// + /// Gets or sets the spacing to add to top and bottom of each tooltip item. + /// + public double? BodySpacing { get; set; } + + /// + /// Gets or sets the footer font. + /// + public string? FooterFontFamily { get; set; } + + /// + /// Gets or sets the footer font size. + /// + public double? FooterFontSize { get; set; } + + /// + /// Gets or sets the footer font style. + /// + public BitChartFontStyle? FooterFontStyle { get; set; } + + /// + /// Gets or sets the footer font color. + /// See for working with colors. + /// + public string? FooterFontColor { get; set; } + + /// + /// Gets or sets the spacing to add to top and bottom of each footer line. + /// + public double? FooterSpacing { get; set; } + + /// + /// Gets or sets the margin to add before drawing the footer. + /// + public double? FooterMarginTop { get; set; } + + /// + /// Gets or sets the padding to add on left and right of tooltip. + /// + public double? XPadding { get; set; } + + /// + /// Gets or sets the padding to add on top and bottom of tooltip. + /// + public double? YPadding { get; set; } + + /// + /// Gets or sets the extra distance to move the end of the tooltip arrow away from the tooltip point. + /// + public double? CaretPadding { get; set; } + + /// + /// Gets or sets the size, in px, of the tooltip arrow. + /// + public double? CaretSize { get; set; } + + /// + /// Gets or sets the radius of tooltip corner curves. + /// + public double? CornerRadius { get; set; } + + /// + /// Gets or sets the color to draw behind the colored boxes when multiple items are in the tooltip. + /// See for working with colors. + /// + public string? MultiKeyBackground { get; set; } + + /// + /// Gets or sets the value indicating if color boxes are shown in the tooltip. + /// + public bool? DisplayColors { get; set; } + + /// + /// Gets or sets the color of the border. + /// See for working with colors. + /// + public string? BorderColor { get; set; } + + /// + /// Gets or sets the size of the border. + /// + public double? BorderWidth { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartAxisDirection.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartAxisDirection.cs new file mode 100644 index 0000000000..71eb8f9ef8 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartAxisDirection.cs @@ -0,0 +1,24 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents the possible axis directions. +/// +public sealed class BitChartAxisDirection : BitChartStringEnum +{ + /// + /// The X-axis direction. + /// + public static BitChartAxisDirection X => new BitChartAxisDirection("x"); + + /// + /// The Y-axis direction. + /// + public static BitChartAxisDirection Y => new BitChartAxisDirection("y"); + + /// + /// Both the X- and Y-axis direction. + /// + public static BitChartAxisDirection XY => new BitChartAxisDirection("xy"); + + private BitChartAxisDirection(string stringRep) : base(stringRep) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartAxisDisplay.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartAxisDisplay.cs new file mode 100644 index 0000000000..e7c54ae076 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartAxisDisplay.cs @@ -0,0 +1,26 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// As per documentation here (Chart.js). +/// +public sealed class BitChartAxisDisplay : BitChartObjectEnum +{ + /// + /// Hidden + /// + public static BitChartAxisDisplay False => new BitChartAxisDisplay(false); + + /// + /// Visible + /// + public static BitChartAxisDisplay True => new BitChartAxisDisplay(true); + + /// + /// Visible only if at least one associated dataset is visible + /// + public static BitChartAxisDisplay Auto => new BitChartAxisDisplay("auto"); + + + private BitChartAxisDisplay(string stringValue) : base(stringValue) { } + private BitChartAxisDisplay(bool boolValue) : base(boolValue) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartAxisType.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartAxisType.cs new file mode 100644 index 0000000000..3b7078cd0f --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartAxisType.cs @@ -0,0 +1,48 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// As per documentation here (Chart.js). +/// +public sealed class BitChartAxisType : BitChartStringEnum +{ + /// + /// The linear scale is use to chart numerical data. + /// As the name suggests, linear interpolation is used to determine where a value lies on the axis. + /// Can be used both for Radial and for Cartesian Axes + /// For cartesian: It can be placed on either the x or y axis. + /// The scatter chart type automatically configures a line chart to use one of these scales for the x axis. + /// + public static BitChartAxisType Linear => new BitChartAxisType("linear"); + + /// + /// The logarithmic scale is use to chart numerical data. It can be placed on either the x or y axis. + /// As the name suggests, logarithmic interpolation is used to determine where a value lies on the axis. + /// + public static BitChartAxisType Logarithmic => new BitChartAxisType("logarithmic"); + + /// + /// If global configuration is used, labels are drawn from one of the label arrays included in the chart data (). + /// If only is defined, this will be used. If is defined and the axis is horizontal, this will be used. + /// Similarly, if is defined and the axis is vertical, this property will be used. + /// Using both xLabels and yLabels together can create a chart that uses strings for both the X and Y axes. + /// Specifying any of the settings above defines the x axis as type: if not defined otherwise. + /// + /// Note: The axes in this library are strongly typed and always contain a type so this inference only applies if you don't provide an axis in the options. + /// + /// + /// For more fine-grained control of category labels it is also possible to add labels as part of the category axis definition (). + /// Doing so does not apply the global defaults for the chart. + /// + /// See here (Chart.js). + /// + public static BitChartAxisType Category => new BitChartAxisType("category"); + + /// + /// The time scale is used to display times and dates. + /// When building its ticks, it will automatically calculate the most comfortable unit base on the size of the scale. + /// + public static BitChartAxisType Time => new BitChartAxisType("time"); + + + private BitChartAxisType(string stringRep) : base(stringRep) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartBorderAlign.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartBorderAlign.cs new file mode 100644 index 0000000000..4aca7e4341 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartBorderAlign.cs @@ -0,0 +1,24 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Specifies the border alignment of a pie chart and a polar area chart. +/// As per documentation here (Chart.js). +/// +public sealed class BitChartBorderAlign : BitChartStringEnum +{ + /// + /// When is set, the borders of arcs next to each other will overlap. The default value. + /// + public static BitChartBorderAlign Center => new BitChartBorderAlign("center"); + + /// + /// When is set, it is guaranteed that all the borders will not overlap. + /// + public static BitChartBorderAlign Inner => new BitChartBorderAlign("inner"); + + /// + /// Creates a new instance of the class. + /// + /// The value to set. + private BitChartBorderAlign(string stringValue) : base(stringValue) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartBorderCapStyle.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartBorderCapStyle.cs new file mode 100644 index 0000000000..7041f11f9f --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartBorderCapStyle.cs @@ -0,0 +1,29 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Specifies the border cap style. +/// As per documentation here (MDN). +/// +public sealed class BitChartBorderCapStyle : BitChartStringEnum +{ + /// + /// The ends of lines are squared off at the endpoints. + /// + public static BitChartBorderCapStyle Butt => new BitChartBorderCapStyle("butt"); + + /// + /// The ends of lines are rounded. + /// + public static BitChartBorderCapStyle Round => new BitChartBorderCapStyle("round"); + + /// + /// The ends of lines are squared off by adding a box with an equal width and half the height of the line's thickness. + /// + public static BitChartBorderCapStyle Square => new BitChartBorderCapStyle("square"); + + /// + /// Creates a new instance of the class. + /// + /// The value to set. + private BitChartBorderCapStyle(string stringValue) : base(stringValue) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartBorderJoinStyle.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartBorderJoinStyle.cs new file mode 100644 index 0000000000..a0352efc41 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartBorderJoinStyle.cs @@ -0,0 +1,29 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Specifies the border join style. +/// As per documentation here (MDN). +/// +public sealed class BitChartBorderJoinStyle : BitChartStringEnum +{ + /// + /// Fills an additional triangular area between the common endpoint of connected segments, and the separate outside rectangular corners of each segment. + /// + public static BitChartBorderJoinStyle Bevel => new BitChartBorderJoinStyle("bevel"); + + /// + /// Rounds off the corners of a shape by filling an additional sector of disc centered at the common endpoint of connected segments. The radius for these rounded corners is equal to the line width. + /// + public static BitChartBorderJoinStyle Round => new BitChartBorderJoinStyle("round"); + + /// + /// Connected segments are joined by extending their outside edges to connect at a single point, with the effect of filling an additional lozenge-shaped area. + /// + public static BitChartBorderJoinStyle Miter => new BitChartBorderJoinStyle("miter"); + + /// + /// Creates a new instance of the class. + /// + /// The value to set. + private BitChartBorderJoinStyle(string stringValue) : base(stringValue) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartBorderSkipped.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartBorderSkipped.cs new file mode 100644 index 0000000000..63eba8aaa3 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartBorderSkipped.cs @@ -0,0 +1,48 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// This setting is used to avoid drawing the bar stroke at the base of the fill. +/// In general, this does not need to be changed except when creating chart types that derive from a bar chart. +/// Note: For negative bars in vertical chart, top and bottom are flipped. Same goes for left and right in horizontal +/// chart. +/// As per documentation here (Chart.js). +/// +public class BitChartBorderSkipped : BitChartObjectEnum +{ + /// + /// Creates a new instance of the class. + /// + /// The value to set. + private BitChartBorderSkipped(string stringValue) : base(stringValue) { } + + /// + /// Creates a new instance of the class. + /// + /// The value to set. + private BitChartBorderSkipped(bool boolValue) : base(boolValue) { } + + /// + /// The bottom border skipped style. + /// + public static BitChartBorderSkipped Bottom => new BitChartBorderSkipped("bottom"); + + /// + /// The false border skipped style. + /// + public static BitChartBorderSkipped False => new BitChartBorderSkipped(false); + + /// + /// The left border skipped style. + /// + public static BitChartBorderSkipped Left => new BitChartBorderSkipped("left"); + + /// + /// The right border skipped style. + /// + public static BitChartBorderSkipped Right => new BitChartBorderSkipped("right"); + + /// + /// The top border skipped style. + /// + public static BitChartBorderSkipped Top => new BitChartBorderSkipped("top"); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartBrowserEvent.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartBrowserEvent.cs new file mode 100644 index 0000000000..667fbecc06 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartBrowserEvent.cs @@ -0,0 +1,88 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a browser event. Not all browser events are listed +/// as properties but you can use to create +/// events that aren't listed as static properties. +/// +/// Reference for browser events can be found here: +/// +/// +/// +public sealed class BitChartBrowserEvent : BitChartStringEnum +{ + /// + /// A pointing device button (ANY button; soon to be primary button only) + /// has been pressed and released on an element. + /// + public static BitChartBrowserEvent Click => new BitChartBrowserEvent("click"); + + /// + /// The right button of the mouse is clicked (before the context menu is displayed). + /// + public static BitChartBrowserEvent ContextMenu => new BitChartBrowserEvent("contextmenu"); + + /// + /// A pointing device button is clicked twice on an element. + /// + public static BitChartBrowserEvent DoubleClick => new BitChartBrowserEvent("dblclick"); + + /// + /// A pointing device button is pressed on an element. + /// + public static BitChartBrowserEvent MouseDown => new BitChartBrowserEvent("mousedown"); + + /// + /// A pointing device is moved onto the element that has the listener attached. + /// + public static BitChartBrowserEvent MouseEnter => new BitChartBrowserEvent("mouseenter"); + + /// + /// A pointing device is moved off the element that has the listener attached. + /// + public static BitChartBrowserEvent MouseLeave => new BitChartBrowserEvent("mouseleave"); + + /// + /// A pointing device is moved over an element. (Fired continously as the mouse moves.) + /// + public static BitChartBrowserEvent MouseMove => new BitChartBrowserEvent("mousemove"); + + /// + /// A pointing device is moved onto the element that has the listener attached or onto one of its children. + /// + public static BitChartBrowserEvent MouseOver => new BitChartBrowserEvent("mouseover"); + + /// + /// A pointing device is moved off the element that has the listener attached or off one of its children. + /// + public static BitChartBrowserEvent MouseOut => new BitChartBrowserEvent("mouseout"); + + /// + /// A pointing device button is released over an element. + /// + public static BitChartBrowserEvent MouseUp => new BitChartBrowserEvent("mouseup"); + + /// + /// One or more touch points are placed on the touch surface. + /// + public static BitChartBrowserEvent TouchStart => new BitChartBrowserEvent("touchstart"); + + /// + /// One or more touch points are moved along the touch surface. + /// + public static BitChartBrowserEvent TouchMove => new BitChartBrowserEvent("touchmove"); + + /// + /// One or more touch points are removed from the touch surface. + /// + public static BitChartBrowserEvent TouchEnd => new BitChartBrowserEvent("touchend"); + + /// + /// This method constructs a which represents the given value. + /// Use this method if the event is not available as a static property. + /// + /// The string representation of your browser event. + public static BitChartBrowserEvent Custom(string browserEvent) => new BitChartBrowserEvent(browserEvent); + + private BitChartBrowserEvent(string stringRep) : base(stringRep) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartChartType.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartChartType.cs new file mode 100644 index 0000000000..89469d2451 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartChartType.cs @@ -0,0 +1,62 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Defines the chart types. +/// +public sealed class BitChartChartType : BitChartStringEnum +{ + /// + /// The bar chart type. + /// + public static BitChartChartType Bar => new BitChartChartType("bar"); + + /// + /// The horizontal bar chart type. + /// + public static BitChartChartType HorizontalBar => new BitChartChartType("horizontalBar"); + + /// + /// The line chart type. + /// + public static BitChartChartType Line => new BitChartChartType("line"); + + /// + /// The pie chart type. + /// + public static BitChartChartType Pie => new BitChartChartType("pie"); + + /// + /// The doughnut chart type. + /// + public static BitChartChartType Doughnut => new BitChartChartType("doughnut"); + + /// + /// The radar chart type. + /// + public static BitChartChartType Radar => new BitChartChartType("radar"); + + /// + /// The bubble chart type. + /// + public static BitChartChartType Bubble => new BitChartChartType("bubble"); + + /// + /// The polar area chart type. + /// + public static BitChartChartType PolarArea => new BitChartChartType("polarArea"); + + /// + /// The scatter chart type. + /// + public static BitChartChartType Scatter => new BitChartChartType("scatter"); + + /// + /// This method constructs a which represents the given value. + /// Only use this method if you're implementing your own chart e.g. for a Chart.js + /// extension. Otherwise use the static properties. + /// + /// The string representation of your custom chart type. + public static BitChartChartType Custom(string customChartType) => new BitChartChartType(customChartType); + + private BitChartChartType(string stringValue) : base(stringValue) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartCubicInterpolationMode.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartCubicInterpolationMode.cs new file mode 100644 index 0000000000..ba27e7a3d7 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartCubicInterpolationMode.cs @@ -0,0 +1,27 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Specifies the cubic interpolation mode. +/// As per documentation here (Chart.js). +/// +public sealed class BitChartCubicInterpolationMode : BitChartStringEnum +{ + /// + /// The default cubic interpolation mode. + /// The algorithm uses a custom weighted cubic interpolation, which produces pleasant curves for all types of datasets. + /// + public static BitChartCubicInterpolationMode Default => new BitChartCubicInterpolationMode("default"); + + /// + /// The monotone cubic interpolation mode. + /// The algorithm is more suited to y = f(x) datasets: + /// It preserves monotonicity (or piecewise monotonicity) of the dataset being interpolated, and ensures local extrema (if any) stay at input data points. + /// + public static BitChartCubicInterpolationMode Monotone => new BitChartCubicInterpolationMode("monotone"); + + /// + /// Creates a new instance of the class. + /// + /// The value to set. + private BitChartCubicInterpolationMode(string stringValue) : base(stringValue) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartEasing.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartEasing.cs new file mode 100644 index 0000000000..b8c020e04c --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartEasing.cs @@ -0,0 +1,45 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents an easing function supported by Chart.js. Details about the different +/// functions can be found here: +/// +/// As per documentation here (Chart.js). +/// +/// +public sealed class BitChartEasing : BitChartStringEnum +{ + public static BitChartEasing Linear => new BitChartEasing("linear"); + public static BitChartEasing EaseInQuad => new BitChartEasing("easeInQuad"); + public static BitChartEasing EaseOutQuad => new BitChartEasing("easeOutQuad"); + public static BitChartEasing EaseInOutQuad => new BitChartEasing("easeInOutQuad"); + public static BitChartEasing EaseInCubic => new BitChartEasing("easeInCubic"); + public static BitChartEasing EaseOutCubic => new BitChartEasing("easeOutCubic"); + public static BitChartEasing EaseInOutCubic => new BitChartEasing("easeInOutCubic"); + public static BitChartEasing EaseInQuart => new BitChartEasing("easeInQuart"); + public static BitChartEasing EaseOutQuart => new BitChartEasing("easeOutQuart"); + public static BitChartEasing EaseInOutQuart => new BitChartEasing("easeInOutQuart"); + public static BitChartEasing EaseInQuint => new BitChartEasing("easeInQuint"); + public static BitChartEasing EaseOutQuint => new BitChartEasing("easeOutQuint"); + public static BitChartEasing EaseInOutQuint => new BitChartEasing("easeInOutQuint"); + public static BitChartEasing EaseInSine => new BitChartEasing("easeInSine"); + public static BitChartEasing EaseOutSine => new BitChartEasing("easeOutSine"); + public static BitChartEasing EaseInOutSine => new BitChartEasing("easeInOutSine"); + public static BitChartEasing EaseInExpo => new BitChartEasing("easeInExpo"); + public static BitChartEasing EaseOutExpo => new BitChartEasing("easeOutExpo"); + public static BitChartEasing EaseInOutExpo => new BitChartEasing("easeInOutExpo"); + public static BitChartEasing EaseInCirc => new BitChartEasing("easeInCirc"); + public static BitChartEasing EaseOutCirc => new BitChartEasing("easeOutCirc"); + public static BitChartEasing EaseInOutCirc => new BitChartEasing("easeInOutCirc"); + public static BitChartEasing EaseInElastic => new BitChartEasing("easeInElastic"); + public static BitChartEasing EaseOutElastic => new BitChartEasing("easeOutElastic"); + public static BitChartEasing EaseInOutElastic => new BitChartEasing("easeInOutElastic"); + public static BitChartEasing EaseInBack => new BitChartEasing("easeInBack"); + public static BitChartEasing EaseOutBack => new BitChartEasing("easeOutBack"); + public static BitChartEasing EaseInOutBack => new BitChartEasing("easeInOutBack"); + public static BitChartEasing EaseInBounce => new BitChartEasing("easeInBounce"); + public static BitChartEasing EaseOutBounce => new BitChartEasing("easeOutBounce"); + public static BitChartEasing EaseInOutBounce => new BitChartEasing("easeInOutBounce"); + + private BitChartEasing(string stringRep) : base(stringRep) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartFillingMode.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartFillingMode.cs new file mode 100644 index 0000000000..529cd99adc --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartFillingMode.cs @@ -0,0 +1,87 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The filling mode is used in area charts (like line and radar) to define how the area around +/// the lines is filled. +/// +/// As per documentation here (Chart.js). +/// +/// +public sealed class BitChartFillingMode : BitChartObjectEnum +{ + /// + /// Gets a that represents filling the area between the current + /// dataset and the dataset at the specified relative index. + /// + /// Example: Specifying -1 for the third dataset in the chart will cause the area between + /// the third and the second dataset to be filled. + /// + /// + /// The relative index of the dataset to fill to. + public static BitChartFillingMode Relative(int relativeDatasetIndex) + { + if (relativeDatasetIndex == 0) + throw new ArgumentOutOfRangeException(nameof(relativeDatasetIndex)); + + if (relativeDatasetIndex < 0) + { + return new BitChartFillingMode(relativeDatasetIndex.ToString()); + } + else + { + return new BitChartFillingMode($"+{relativeDatasetIndex}"); + } + } + + /// + /// Gets a that represents filling the area between the current + /// dataset and the dataset at the specified (zero-based) index. + /// + /// Example: Specifying 1 for the third dataset in the chart will cause the area between + /// the third and the second dataset to be filled. + /// + /// + /// The absolute (zero-based) index of the dataset to fill to. + public static BitChartFillingMode Absolute(int absoluteDatasetIndex) + { + if (absoluteDatasetIndex < 0) + throw new ArgumentOutOfRangeException(nameof(absoluteDatasetIndex)); + + return new BitChartFillingMode(absoluteDatasetIndex); + } + + /// + /// Gets a that represents no filling. + /// + public static BitChartFillingMode Disabled => new BitChartFillingMode(false); + + /// + /// Gets a that represents filling the area between + /// the current dataset and the origin. + /// + public static BitChartFillingMode Origin => new BitChartFillingMode("origin"); + + /// + /// Gets a that represents filling the area between + /// the current dataset and the start. + /// + public static BitChartFillingMode Start => new BitChartFillingMode("start"); + + /// + /// Gets a that represents filling the area between + /// the current dataset and the end. + /// + public static BitChartFillingMode End => new BitChartFillingMode("end"); + + /// + /// Converts a value to a value. + /// is equal to and + /// is equal to . + /// + /// A value indicating whether or not to fill the area. + public static implicit operator BitChartFillingMode(bool filled) => new BitChartFillingMode(filled); + + private BitChartFillingMode(int value) : base(value) { } + private BitChartFillingMode(string value) : base(value) { } + private BitChartFillingMode(bool value) : base(value) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartFontStyle.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartFontStyle.cs new file mode 100644 index 0000000000..235c6b9ab8 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartFontStyle.cs @@ -0,0 +1,39 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Specifies the css font style of text. +/// As per documentation here (MDN). +/// +public sealed class BitChartFontStyle : BitChartStringEnum +{ + /// + /// Specifies a font that is classified as normal within a font-family. + /// + public static BitChartFontStyle Normal => new BitChartFontStyle("normal"); + + /// + /// Specifies a font that is classified as italic. If no italic version of the face is available, one classified as oblique is used instead. If neither is available, the style is artificially simulated. + /// + public static BitChartFontStyle Italic => new BitChartFontStyle("italic"); + + /// + /// Specifies a font that is classified as oblique. If no oblique version of the face is available, one classified as italic is used instead. If neither is available, the style is artificially simulated. + /// + public static BitChartFontStyle Oblique => new BitChartFontStyle("oblique"); + + /// + /// Specifies a font with its font style set to its default value. + /// + public static BitChartFontStyle Initial => new BitChartFontStyle("initial"); + + /// + /// Specifies a font that inherits the font style from its parent element. + /// + public static BitChartFontStyle Inherit => new BitChartFontStyle("inherit"); + + /// + /// Creates a new instance of the class. + /// + /// The value to set. + private BitChartFontStyle(string stringValue) : base(stringValue) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartInteractionMode.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartInteractionMode.cs new file mode 100644 index 0000000000..6c25888897 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartInteractionMode.cs @@ -0,0 +1,43 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// As per documentation here (Chart.js). +/// +public sealed class BitChartInteractionMode : BitChartStringEnum +{ + /// + /// Finds all of the items that intersect the point. + /// + public static BitChartInteractionMode Point => new BitChartInteractionMode("point"); + + /// + /// Gets the items that are at the nearest distance to the point. + /// The nearest item is determined based on the distance to the center of the chart item (point, bar). + /// + public static BitChartInteractionMode Nearest => new BitChartInteractionMode("nearest"); + + /// + /// Finds item at the same index. + /// + public static BitChartInteractionMode Index => new BitChartInteractionMode("index"); + + /// + /// Finds items in the same dataset. + /// + public static BitChartInteractionMode Dataset => new BitChartInteractionMode("dataset"); + + /// + /// Returns all items that would intersect based on the X coordinate of the position only. + /// Would be useful for a vertical cursor implementation. + /// Note that this only applies to cartesian charts. + /// + public static BitChartInteractionMode X => new BitChartInteractionMode("x"); + + /// + /// Returns all items that would intersect based on the Y coordinate of the position. This would be useful for a horizontal cursor implementation + /// Note that this only applies to cartesian charts. + /// + public static BitChartInteractionMode Y => new BitChartInteractionMode("y"); + + private BitChartInteractionMode(string stringRep) : base(stringRep) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartObjectEnum.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartObjectEnum.cs new file mode 100644 index 0000000000..40635d84e6 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartObjectEnum.cs @@ -0,0 +1,115 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The base class for enums that can represent different types. We also use these +/// "enums" like Discriminated Unions to provide a type safe way of communicating with the +/// dynamic language JavaScript is. +/// +/// De-/serialization is supported but only for the following types: +/// , , and . +/// For the deserialization, the constructors with a single parameter of a supported type +/// are considered for instantiating the object enum. +/// +/// +/// When implementing an object enum, make sure to provide only private constructors +/// with the types that are allowed (DO NOT expose public constructors; expose meaningful +/// static factory methods instead). The actual enum values are static properties that pass +/// the correct value to the private constructor. Make these properties return new values +/// everytime so we don't create all the enum values even though we don't use them. +/// In the classic use case, we don't call many of these properties anyway and usually +/// only a few times. You can also have static factory methods that +/// create an instance of the object enum with the specified value as long as the parameter +/// type is supported. Also consider sealing your enum unless you have a specific reason not to. +/// +/// +[Newtonsoft.Json.JsonConverter(typeof(JsonObjectEnumConverter))] +public abstract class BitChartObjectEnum : IEquatable +{ + /// + /// Gets the s that are supported for serialization and deserialization. + /// can contain objects of different types but you will get a + /// once you try to serialize (or deserialize) that + /// . + /// + private static readonly Type[] _supportedSerializationTypes = new[] + { + typeof(int), typeof(double), typeof(string), typeof(bool) + }; + + /// + /// Holds the actual value represented by this instance. + /// + internal object Value { get; } + + /// + /// Creates a new instance of . + /// + /// The value this instance is supposed to represent. + protected BitChartObjectEnum(object value) + { + Value = value ?? throw new ArgumentNullException(nameof(value)); + + if (value is BitChartObjectEnum) + throw new ArgumentException("The value cannot be an ObjectEnum. Recursive ObjectEnums aren't allowed."); + } + + /// + /// Checks if a is in the list of supported serialization types. + /// If this function returns , de-/serialization will fail on + /// s containing an instance of that + /// (). + /// + /// The to check. + internal static bool IsSupportedSerializationType(Type type) => + _supportedSerializationTypes.Contains(type); + + /// + /// Determines whether the specified object is considered equal to the current object. + /// + /// is considered to be equal to this instance if it.. + /// + /// is the same instance as this instance. + /// is another with the same internal value. + /// is the same value as the internal value of this . + /// + /// + /// + /// The object to compare with the current object. + /// true if the specified object is considered to be equal to the current object; + /// otherwise, false. + public override bool Equals(object? obj) + { + if (obj is BitChartObjectEnum asEnum) + { + return Equals(asEnum); + } + + return Value.Equals(obj); + } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// An object to compare with this object. + /// true if the current object is equal to the other parameter; otherwise, false. + /// + public bool Equals(BitChartObjectEnum? other) => other is not null && Value.Equals(other.Value); + + /// + /// Returns the hash code of the underlying value. + /// + /// The hash code of the underlying value. + public override int GetHashCode() => Value.GetHashCode(); + + /// + /// Returns the representation of the underlying object. + /// Calls on the underlying object. + /// + /// The representation of the underlying object. + public override string? ToString() => Value.ToString(); + + public static bool operator ==(BitChartObjectEnum left, BitChartObjectEnum right) => + EqualityComparer.Default.Equals(left?.Value, right?.Value); + + public static bool operator !=(BitChartObjectEnum left, BitChartObjectEnum right) => !(left == right); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartPointStyle.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartPointStyle.cs new file mode 100644 index 0000000000..9c1973d70c --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartPointStyle.cs @@ -0,0 +1,68 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Specifies how a data-point on the chart will be styled. +/// As per documentation here (Chart.js). +/// Some samples showcasing the different styles can be found here (Chart.js). +/// +public sealed class BitChartPointStyle : BitChartStringEnum +{ + /// + /// The circle point style. + /// + public static BitChartPointStyle Circle => new BitChartPointStyle("circle"); + + /// + /// The cross point style. + /// + public static BitChartPointStyle Cross => new BitChartPointStyle("cross"); + + /// + /// The rotated (45°) cross point style. + /// + public static BitChartPointStyle CrossRot => new BitChartPointStyle("crossRot"); + + /// + /// The dash point style. + /// Unlike , this style only displayes a dash on the right side of the point. + /// + public static BitChartPointStyle Dash => new BitChartPointStyle("dash"); + + /// + /// The line point style. + /// Unlike , this style displayes a dash on both the right and the left side of the point. + /// + public static BitChartPointStyle Line => new BitChartPointStyle("line"); + + /// + /// The rectangle point style. + /// + public static BitChartPointStyle Rect => new BitChartPointStyle("rect"); + + /// + /// The rounded rectangle point style. + /// + public static BitChartPointStyle RectRounded => new BitChartPointStyle("rectRounded"); + + /// + /// The rotated (45°) rectangle point style. + /// + public static BitChartPointStyle RectRot => new BitChartPointStyle("rectRot"); + + /// + /// The star point style. + /// Same as and overlapping. + /// + public static BitChartPointStyle Star => new BitChartPointStyle("star"); + + /// + /// The triangle point style. + /// + public static BitChartPointStyle Triangle => new BitChartPointStyle("triangle"); + + /// + /// Creates a new instance of the class. + /// + /// The value to set. + private BitChartPointStyle(string stringValue) : base(stringValue) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartPosition.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartPosition.cs new file mode 100644 index 0000000000..d594020fba --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartPosition.cs @@ -0,0 +1,14 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a relative direction or position on a 2D canvas. +/// +public sealed class BitChartPosition : BitChartStringEnum +{ + public static BitChartPosition Left => new BitChartPosition("left"); + public static BitChartPosition Right => new BitChartPosition("right"); + public static BitChartPosition Top => new BitChartPosition("top"); + public static BitChartPosition Bottom => new BitChartPosition("bottom"); + + private BitChartPosition(string stringRep) : base(stringRep) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartScaleBound.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartScaleBound.cs new file mode 100644 index 0000000000..00deeb26a8 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartScaleBound.cs @@ -0,0 +1,20 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Specifies the scale boundary strategy. +/// As per documentation here (Chart.js). +/// +public sealed class BitChartScaleBound : BitChartStringEnum +{ + /// + /// Makes sure data are fully visible, labels outside are removed. + /// + public static BitChartScaleBound Data => new BitChartScaleBound("data"); + + /// + /// Makes sure ticks are fully visible, data outside are truncated. + /// + public static BitChartScaleBound Ticks => new BitChartScaleBound("ticks"); + + private BitChartScaleBound(string stringRep) : base(stringRep) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartSteppedLine.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartSteppedLine.cs new file mode 100644 index 0000000000..3839605298 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartSteppedLine.cs @@ -0,0 +1,35 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// As per documentation here (Chart.js). +/// +public sealed class BitChartSteppedLine : BitChartObjectEnum +{ + /// + /// No Step Interpolation (default) + /// + public static BitChartSteppedLine False => new BitChartSteppedLine(false); + + /// + /// Step-before Interpolation (same as ) + /// + public static BitChartSteppedLine True => new BitChartSteppedLine(true); + + /// + /// Step-before Interpolation + /// + public static BitChartSteppedLine Before => new BitChartSteppedLine("before"); + + /// + /// Step-after Interpolation + /// + public static BitChartSteppedLine After => new BitChartSteppedLine("after"); + + /// + /// Step-middle Interpolation + /// + public static BitChartSteppedLine Middle => new BitChartSteppedLine("middle"); + + private BitChartSteppedLine(string stringValue) : base(stringValue) { } + private BitChartSteppedLine(bool boolValue) : base(boolValue) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartStringEnum.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartStringEnum.cs new file mode 100644 index 0000000000..1df94807e1 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartStringEnum.cs @@ -0,0 +1,90 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The base class for enums that are meant to be serialized. They are more flexible +/// than normal C# enums (through type safe enum pattern). +/// +/// When implementing a , make sure to only implement a single +/// constructor that takes a single . Make this constructor private! +/// The actual enum values are static properties that pass the correct value to the private +/// constructor. Make these properties return new values everytime so we don't create all +/// the enum values even though we don't use them. In the classic use case, we don't call +/// many of these properties anyway and usually only a few times. +/// In the rare case that you need a that can contain any +/// value, expose a static factory method but don't make the constructor +/// public. Also consider sealing your enum unless you have a specific reason not to. +/// +/// +[Newtonsoft.Json.JsonConverter(typeof(JsonStringEnumConverter))] +public abstract class BitChartStringEnum : IEquatable +{ + private readonly string _value; + + /// + /// Creates a new instance of . + /// + /// The this instance should represent. + protected BitChartStringEnum(string stringRep) + { + _value = stringRep ?? throw new ArgumentNullException(nameof(stringRep)); + } + + /// + /// Determines whether the specified object is considered equal to the current object. + /// + /// is considered to be equal to this instance if it.. + /// + /// is the same instance as this instance. + /// is another with the same internal + /// value. + /// is the same value as the internal value + /// of this . + /// + /// + /// + /// The object to compare with the current object. + /// true if the specified object is considered to be equal to the current + /// object; otherwise, false. + public override bool Equals(object? obj) + { + if (obj is BitChartStringEnum asEnum) + { + return Equals(asEnum); + } + + if (obj is string asString) + { + return _value == asString; + } + + return false; + } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// An object to compare with this object. + /// true if the current object is equal to the other parameter; + /// otherwise, false. + public bool Equals(BitChartStringEnum? other) => other is not null && _value == other._value; + + /// + /// Returns the hash code of the underlying value. + /// + /// The hash code of the underlying value. + public override int GetHashCode() => _value.GetHashCode(); + + /// + /// Returns the underlying value. + /// + /// The underlying value. + public override string ToString() => _value; + + public static bool operator ==(BitChartStringEnum left, BitChartStringEnum right) => + left?._value == right?._value; + + public static bool operator !=(BitChartStringEnum left, BitChartStringEnum right) => + left?._value != right?._value; + + public static explicit operator string(BitChartStringEnum stringEnum) => stringEnum._value; +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartTickSource.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartTickSource.cs new file mode 100644 index 0000000000..e69d2902a5 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartTickSource.cs @@ -0,0 +1,25 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// As per documentation here (Chart.js).. +/// +public sealed class BitChartTickSource : BitChartStringEnum +{ + /// + /// Generates "optimal" ticks based on scale size and time options. + /// + public static BitChartTickSource Auto => new BitChartTickSource("auto"); + + /// + /// Generates ticks from data (including labels from data {t|x|y} objects). + /// + public static BitChartTickSource Data => new BitChartTickSource("data"); + + /// + /// Generates ticks from user given values ONLY. + /// + public static BitChartTickSource Labels => new BitChartTickSource("labels"); + + + private BitChartTickSource(string stringRep) : base(stringRep) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartTimeDistribution.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartTimeDistribution.cs new file mode 100644 index 0000000000..eddbfb9d50 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartTimeDistribution.cs @@ -0,0 +1,20 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// As per documentation here (Chart.js). +/// +public sealed class BitChartTimeDistribution : BitChartStringEnum +{ + /// + /// Data are spread according to their time (distances can vary) + /// + public static BitChartTimeDistribution Linear => new BitChartTimeDistribution("linear"); + + /// + /// Data are spread at the same distance from each other + /// + public static BitChartTimeDistribution Series => new BitChartTimeDistribution("series"); + + + private BitChartTimeDistribution(string stringRep) : base(stringRep) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartTimeMeasurement.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartTimeMeasurement.cs new file mode 100644 index 0000000000..5038940051 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartTimeMeasurement.cs @@ -0,0 +1,19 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// As per documentation here (Chart.js). +/// +public sealed class BitChartTimeMeasurement : BitChartStringEnum +{ + public static BitChartTimeMeasurement Millisecond => new BitChartTimeMeasurement("millisecond"); + public static BitChartTimeMeasurement Second => new BitChartTimeMeasurement("second"); + public static BitChartTimeMeasurement Minute => new BitChartTimeMeasurement("minute"); + public static BitChartTimeMeasurement Hour => new BitChartTimeMeasurement("hour"); + public static BitChartTimeMeasurement Day => new BitChartTimeMeasurement("day"); + public static BitChartTimeMeasurement Week => new BitChartTimeMeasurement("week"); + public static BitChartTimeMeasurement Month => new BitChartTimeMeasurement("month"); + public static BitChartTimeMeasurement Quarter => new BitChartTimeMeasurement("quarter"); + public static BitChartTimeMeasurement Year => new BitChartTimeMeasurement("year"); + + private BitChartTimeMeasurement(string stringRep) : base(stringRep) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartTooltipPosition.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartTooltipPosition.cs new file mode 100644 index 0000000000..9a08d45ce8 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/BitChartTooltipPosition.cs @@ -0,0 +1,20 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Specifies where the tooltip will be displayed. +/// As per documentation here (Chart.js). +/// +public sealed class BitChartTooltipPosition : BitChartStringEnum +{ + /// + /// When is set, the tooltip will be placed at the average position of the items displayed in the tooltip. + /// + public static BitChartTooltipPosition Average => new BitChartTooltipPosition("average"); + + /// + /// When is set, the tooltip will be placed at the position of the element closest to the event position. + /// + public static BitChartTooltipPosition Nearest => new BitChartTooltipPosition("nearest"); + + private BitChartTooltipPosition(string stringRep) : base(stringRep) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/Serialization/JsonObjectEnumConverter.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/Serialization/JsonObjectEnumConverter.cs new file mode 100644 index 0000000000..e538d11b2e --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/Serialization/JsonObjectEnumConverter.cs @@ -0,0 +1,96 @@ +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; + +namespace Bit.BlazorUI.Legacy; + +internal class JsonObjectEnumConverter : JsonConverter +{ + public override BitChartObjectEnum? ReadJson(JsonReader reader, Type objectType, [AllowNull] BitChartObjectEnum existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null || + reader.TokenType == JsonToken.Undefined) + { + return null; + } + + if (reader.Value == null) + { + /* Covers all token types that result in reader.Value not being assigned (yet) except null and undefined + * Examples are: StartArray, StartObject, .. + */ + throw new NotSupportedException($"Deserializing ObjectEnums from token type '{reader.TokenType}' isn't supported."); + } + + object value = reader.Value; + Type readerValueType = value.GetType(); + /* The Type of reader.Value isn't always what it was when it was serialized. + * An issue pointing that out: https://github.com/dotnet/orleans/issues/1269#issuecomment-171233788 + * - Any integer number will be of type System.Int64 unless its smaller + * than Int64.MinValue or higher than Int64.MaxValue, then it will be of type System.Numerics.BigInteger + * - Any non-integer number will be of type System.Double + * + * Currently we cast down long to int and ignore BigInteger. This means that only int is supported + * and we don't waste time checking for other options and converting between types. + * + * Another option would be to check for a suitable constructor and if there isn't one try to find the + * most optimal conversion. Even though that sounds nice, it's really not necessary at the moment. There's + * no need for BigInteger support in the enums and it would hurt performance a bit. + */ + + ObjectEnumFactory factory = ObjectEnumFactory.GetFactory(objectType); + + // special case for long since json.net's default for number deserialization is long but our enums + // use (and only support) int at the moment. BigInteger isn't supported at all and will throw on the next check. + if (readerValueType == typeof(long)) + { + long asLong = (long)value; + if (asLong < int.MinValue || + asLong > int.MaxValue) + { + throw new OverflowException($"The deserialized number ({value}) is out of the range of int ({int.MinValue} - {int.MaxValue})."); + } + else + { + value = (int)asLong; + readerValueType = typeof(int); + } + } + + if (!factory.CanConvertFrom(readerValueType)) + { + if (readerValueType == typeof(int) && + factory.CanConvertFrom(typeof(double))) + { + /* Sometimes a value like "0" or "10" might get deserialized as int even though + * the ObjectEnum meant to handle a double. In that case, we can convert + * the int value to a double and create the enum value from there. + */ + value = (double)(int)value; // both casts are required! (else InvalidCastException) + readerValueType = typeof(double); + } + else + { + throw new NotSupportedException($"Deserialization {nameof(BitChartObjectEnum)} '{objectType.FullName}' from '{readerValueType.Name}' isn't supported."); + } + } + + return factory.Create(value, readerValueType); + } + + public override void WriteJson(JsonWriter writer, BitChartObjectEnum? wrapper, JsonSerializer serializer) + { + ArgumentNullException.ThrowIfNull(wrapper); + + // Note: wrapper won't be null (json.net wouldn't call this method if it were null) + var wrappedType = wrapper.Value.GetType(); + if (!BitChartObjectEnum.IsSupportedSerializationType(wrappedType)) + { + throw new NotSupportedException($"The type '{wrappedType.FullName}' isn't supported for serialization " + + $"within an instance of any {nameof(BitChartObjectEnum)}-type."); + } + + // The types we support can always be written in a single Token. + // If that was not the case, we'd need to handle JsonWriterException here. + writer.WriteValue(wrapper.Value); + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/Serialization/JsonStringEnumConverter.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/Serialization/JsonStringEnumConverter.cs new file mode 100644 index 0000000000..3f34d160da --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/Serialization/JsonStringEnumConverter.cs @@ -0,0 +1,41 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Newtonsoft.Json; + +namespace Bit.BlazorUI.Legacy; + +internal class JsonStringEnumConverter : JsonConverter +{ + private static readonly Type[] _stringParameterArray = [typeof(string)]; + private static readonly ConcurrentDictionary _constructorCache = new(); + + [SuppressMessage("Trimming", "IL2111:Method with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the method.", Justification = "")] + public override BitChartStringEnum? ReadJson(JsonReader reader, Type objectType, [AllowNull] BitChartStringEnum existingValue, bool hasExistingValue, JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.Null: + case JsonToken.Undefined: + return null; + case JsonToken.String: + var constructor = _constructorCache.GetOrAdd(objectType, GetStringConstructor); + + return (BitChartStringEnum?)constructor?.Invoke([reader.Value]); + default: + throw new NotSupportedException($"Deserializing StringEnums from token type '{reader.TokenType}' isn't supported."); + } + } + + public override void WriteJson(JsonWriter writer, BitChartStringEnum? value, JsonSerializer serializer) + { + ArgumentNullException.ThrowIfNull(value); + + // Note: value won't be null (json.net wouldn't call this method if it were null) + // ToString was overwritten by StringEnum -> safe to just print the string representation + writer.WriteValue(value.ToString()); + } + + private ConstructorInfo? GetStringConstructor([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type) => + type.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, _stringParameterArray, null); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/Serialization/ObjectEnumFactory.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/Serialization/ObjectEnumFactory.cs new file mode 100644 index 0000000000..f9c54f11e8 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Enums/Serialization/ObjectEnumFactory.cs @@ -0,0 +1,115 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace Bit.BlazorUI.Legacy; + +/* We favour using a non-generic design here because the "entry point" where this class is used + * is a JsonConverter that has to work for all types of ObjectEnum. Therefore the converter isn't + * generic and we don't have a generic parameter to begin with. We would need to use reflection + * to create the factory and if we just work with Type, we can reduce the reflection use a bit. + */ +internal class ObjectEnumFactory +{ + private static readonly ConcurrentDictionary _factorySingletons = new ConcurrentDictionary(); + + private readonly Dictionary _constructorCache; + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicConstructors)] + private readonly Type _enumType; + + /// + /// Gets (and creates if needed) the singleton-factory for this . + /// + /// The -type whose factory to get. + [SuppressMessage("Trimming", "IL2067:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The parameter of method does not have matching annotations.", Justification = "")] + public static ObjectEnumFactory GetFactory(Type? enumType) + { + ArgumentNullException.ThrowIfNull(enumType); + + if (!typeof(BitChartObjectEnum).IsAssignableFrom(enumType)) + throw new ArgumentException($"The type '{enumType.FullName}' doesn't inherit from '{typeof(BitChartObjectEnum).FullName}'"); + + return _factorySingletons.GetOrAdd(enumType, type => new ObjectEnumFactory(type)); + } + + private ObjectEnumFactory([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type enumType) + { + // checks omitted because the constructor is non-public + _enumType = enumType; + _constructorCache = CreateConstructorDictionary(); + } + + /// + /// Creates a new instance of the -type this factory is for. + /// If there is no suitable constructor for the , a + /// will be thrown. + /// + /// The value used for instantiating the . + public BitChartObjectEnum Create(object value) => Create(value, value.GetType()); + + /// + /// Creates a new instance of the -type this factory is for. + /// If there is no suitable constructor for the type , a + /// will be thrown. + /// + /// Use this method if the type of is already known + /// (and you're sure about it). + /// + /// + /// The value used for instantiating the . + /// The of . + public BitChartObjectEnum Create(object value, Type valueType) + { + if (_constructorCache.TryGetValue(valueType, out ConstructorInfo? constructor)) + { + return (BitChartObjectEnum)constructor.Invoke([value]); + } + + if (BitChartObjectEnum.IsSupportedSerializationType(valueType)) + { + throw new NotSupportedException($"The object enum '{_enumType.FullName}' doesn't have a constructor which takes a single " + + $"argument of type '{valueType.FullName}'."); + } + else + { + throw new NotSupportedException($"The type '{valueType}' isn't supported for serialization within {nameof(BitChartObjectEnum)}."); + } + } + + /// + /// Checks if a suitable constructor for this exists which + /// can be used to create a new instance of that -type. + /// + /// The of the enum-content to look for. + /// if there is a suitable constructor for that ; + /// otherwise . + public bool CanConvertFrom(Type contentType) => _constructorCache.ContainsKey(contentType); + + private Dictionary CreateConstructorDictionary() + { + Dictionary dict = new Dictionary(); + ConstructorInfo[] constructors = _enumType.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic); + foreach (ConstructorInfo constructor in constructors) + { + ParameterInfo[] constructorParams = constructor.GetParameters(); + if (constructorParams.Length != 1) + { + continue; + } + + Type paramType = constructorParams[0].ParameterType; + if (BitChartObjectEnum.IsSupportedSerializationType(paramType)) + { + dict.Add(paramType, constructor); + } + } + + if (dict.Count == 0) + { + throw new NotSupportedException($"The {nameof(BitChartObjectEnum)} type '{_enumType.FullName}' doesn't have any " + + $"suitable constructors for deserialization."); + } + + return dict; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Handlers/BitChartAxisTickCallback.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Handlers/BitChartAxisTickCallback.cs new file mode 100644 index 0000000000..c6ab292cbb --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Handlers/BitChartAxisTickCallback.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json.Linq; + +namespace Bit.BlazorUI.Legacy; + +/// +/// A delegate used for customizing tick marks on an axis. +/// If this callback returns (or undefined), the associated grid line will be hidden. +/// +/// The value. It's usually a +/// but can also be other types like (e.g. in a category axis). +/// The index of the tick mark. +/// An array of labels. Normally those are just strings but in a +/// they have a 'label' () and a 'major' () field. +/// The new value of the tick mark or if you want to hide that grid line. +public delegate string BitChartAxisTickCallback(JValue value, int index, JArray values); diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Handlers/BitChartChartMouseEvent.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Handlers/BitChartChartMouseEvent.cs new file mode 100644 index 0000000000..869a4542ff --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Handlers/BitChartChartMouseEvent.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json.Linq; + +namespace Bit.BlazorUI.Legacy; + +/// +/// A delegate for all sorts of mouse events on a chart. +/// +/// The mouse event. +/// The active elements. +public delegate void BitChartChartMouseEvent(JObject mouseEvent, JArray activeElements); diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Handlers/BitChartLegendItemMouseEvent.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Handlers/BitChartLegendItemMouseEvent.cs new file mode 100644 index 0000000000..170a3b574a --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Handlers/BitChartLegendItemMouseEvent.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json.Linq; + +namespace Bit.BlazorUI.Legacy; + +/// +/// A delegate for all sorts of mouse events on a legend item. +/// +/// The mouse event. +/// The affected legend item. +public delegate void BitChartLegendItemMouseEvent(JObject mouseEvent, BitChartLegendItem legendItem); diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Handlers/BitChartLegendLabelFilter.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Handlers/BitChartLegendLabelFilter.cs new file mode 100644 index 0000000000..955adfc93d --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Handlers/BitChartLegendLabelFilter.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json.Linq; + +namespace Bit.BlazorUI.Legacy; + +/// +/// A filter for legend items. Should return if you want to show the legend item; +/// if not. +/// +/// The to either include or filter out. +/// The chart data. This object is large so consider applying a +/// if you don't use the value. +/// if you want to show the legend item; if not. +public delegate bool BitChartLegendLabelFilter(BitChartLegendItem legendItem, JObject chartData); diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Handlers/BitChartLegendLabelsGenerator.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Handlers/BitChartLegendLabelsGenerator.cs new file mode 100644 index 0000000000..78aa2293fa --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Handlers/BitChartLegendLabelsGenerator.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json.Linq; + +namespace Bit.BlazorUI.Legacy; + +/// +/// A generator for legend labels. +/// +/// The chart for which to generate the labels. +/// The s the chart should display. +public delegate ICollection BitChartLegendLabelsGenerator(JObject chart); diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/IBitChartDataset.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/IBitChartDataset.cs new file mode 100644 index 0000000000..7e75af245a --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/IBitChartDataset.cs @@ -0,0 +1,32 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a dataset with an id and a type. +/// +public interface IBitChartDataset +{ + /// + /// Gets the ID of this dataset. Used to keep track of the datasets + /// across the .NET <-> JavaScript boundary. + /// + string Id { get; } + + /// + /// Gets the this dataset is for. + /// Important to set in mixed charts. + /// + BitChartChartType Type { get; } +} + +/// +/// Represents a strongly typed dataset that holds data of type . +/// +/// The type of data this dataset contains. +public interface IDataset : IBitChartDataset, IList +{ + /// + /// Gets the data contained in this dataset. This property is read-only. + /// This is in addition to implementing . + /// + IReadOnlyList Data { get; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/IgnoreDatasetCountContractResolver.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/IgnoreDatasetCountContractResolver.cs new file mode 100644 index 0000000000..ba829715c6 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/IgnoreDatasetCountContractResolver.cs @@ -0,0 +1,36 @@ +using System.Collections; +using System.Collections.ObjectModel; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Bit.BlazorUI.Legacy; + +internal class IgnoreDatasetCountContractResolver : DefaultContractResolver +{ + protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) + { + IList baseProps = base.CreateProperties(type, memberSerialization); + + if (typeof(IBitChartDataset).IsAssignableFrom(type)) + { + string countName = nameof(ICollection.Count); + if (NamingStrategy != null) + { + countName = NamingStrategy.GetPropertyName(countName, false); + } + + foreach (var prop in baseProps) + { + if (prop.PropertyName == countName && + prop.DeclaringType!.IsGenericType && + prop.DeclaringType.GetGenericTypeDefinition() == typeof(Collection<>)) + { + prop.Ignored = true; + break; + } + } + } + + return baseProps; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/IndexableOptionConverter.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/IndexableOptionConverter.cs new file mode 100644 index 0000000000..0057dd9d00 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/IndexableOptionConverter.cs @@ -0,0 +1,51 @@ +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Bit.BlazorUI.Legacy; + +internal class IndexableOptionConverter : JsonConverter +{ + public override bool CanRead => false; + public override bool CanWrite => true; + + public override bool CanConvert(Type objectType) + { + if (!objectType.IsGenericType) return false; + + return objectType.GetGenericTypeDefinition() == typeof(BitChartIndexableOption<>); + } + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "")] + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + ArgumentNullException.ThrowIfNull(value); + + // get the correct property using reflection + var prop = value.GetType().GetProperty(BitChartIndexableOption.PropertyName, BindingFlags.Instance | BindingFlags.NonPublic); + + + try + { + // if the indexable option was not initialized, this will throw an InvalidOperationException wrapped in a TargetInvocationException + var toWrite = prop?.GetValue(value); + + // write the property + JToken.FromObject(toWrite!).WriteTo(writer); + } + catch (TargetInvocationException oex) + when (oex.InnerException is InvalidOperationException iex) + { + Console.WriteLine("Error while trying to serialize an indexable option:"); + Console.WriteLine(iex.Message); + + // write undefined value, chart.js will use their default + writer.WriteUndefined(); + } + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/JsonWriteOnlyConverter.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/JsonWriteOnlyConverter.cs new file mode 100644 index 0000000000..b0be8df5f2 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/JsonWriteOnlyConverter.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Bit.BlazorUI.Legacy; + +internal abstract class JsonWriteOnlyConverter : JsonConverter +{ + public sealed override bool CanRead => false; + public sealed override bool CanWrite => true; + + public sealed override T ReadJson(JsonReader reader, Type objectType, T? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + throw new NotImplementedException("Don't use me to read JSON"); + } + + public abstract override void WriteJson(JsonWriter writer, T? value, JsonSerializer serializer); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Time/BitChartTimeOptions.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Time/BitChartTimeOptions.cs new file mode 100644 index 0000000000..1cae7bab12 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Time/BitChartTimeOptions.cs @@ -0,0 +1,53 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// As per documentation here (Chart.js). +/// +public class BitChartTimeOptions +{ + /// + /// Sets how different time units are displayed. See for more details. + /// + public Dictionary? DisplayFormats { get; set; } + + /// + /// If true and the is set to , then the first day of the week will be Monday. Otherwise, it will be Sunday. + /// + public bool? IsoWeek { get; set; } + + /// + /// If defined, this will override the data maximum. + /// + public DateTime? Max { get; set; } + + /// + /// If defined, this will override the data minimum. + /// + public DateTime? Min { get; set; } + + /// + /// If defined, dates will be rounded to the start of this unit. + /// + public BitChartTimeMeasurement? Round { get; set; } + + /// + /// The Moment.js format string to use for the tooltip. + /// See for possible formats + /// + public string? TooltipFormat { get; set; } + + /// + /// If defined, will force the unit to be a certain type. + /// + public BitChartTimeMeasurement? Unit { get; set; } + + /// + /// The number of units between grid lines. + /// + public int? StepSize { get; set; } + + /// + /// The minimum display format to be used for a time unit. + /// + public BitChartTimeMeasurement? MinUnit { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Time/BitChartTimePoint.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Time/BitChartTimePoint.cs new file mode 100644 index 0000000000..5634ebc4d2 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Common/Time/BitChartTimePoint.cs @@ -0,0 +1,39 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a point on a plane with an X and a Y-value where the +/// X-value is represented by a . +/// Should be used together with a . +/// +public readonly struct BitChartTimePoint : IEquatable +{ + /// + /// Gets the time-value of this . + /// It represents the X-value and will be serialized as 't'. + /// + [Newtonsoft.Json.JsonProperty("t")] + public DateTime Time { get; } + + /// + /// Gets the Y-value of this . + /// + public double Y { get; } + + /// + /// Creates a new instance of . + /// + /// The X / time-value for this . + /// The Y-value for this . + public BitChartTimePoint(DateTime time, double y) + { + Time = time; + Y = y; + } + + public override bool Equals(object? obj) => obj is BitChartTimePoint point && Equals(point); + public bool Equals(BitChartTimePoint other) => Time == other.Time && Y == other.Y; + public override int GetHashCode() => HashCode.Combine(Time, Y); + + public static bool operator ==(BitChartTimePoint left, BitChartTimePoint right) => left.Equals(right); + public static bool operator !=(BitChartTimePoint left, BitChartTimePoint right) => !(left == right); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/JsInterop/BitChartDelegateHandler.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/JsInterop/BitChartDelegateHandler.cs new file mode 100644 index 0000000000..f6d2f23167 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/JsInterop/BitChartDelegateHandler.cs @@ -0,0 +1,141 @@ +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a C#-delegate which can be called by JavaScript. +/// +/// The type of the delegate you want to invoke from JavaScript. +// This class will be serialized by System.Text.Json in the end since we restore the objects +// before passing them to IJsRuntime. Therefore fields to serialize have to be public properties. +public sealed class BitChartDelegateHandler<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T> : IBitChartMethodHandler, IDisposable + where T : Delegate +{ + private static readonly ParameterInfo[] _delegateParameters; + private static readonly bool _delegateHasReturnValue; + + private readonly T _function; + private readonly List _ignoredIndices; + + /// + /// Gets the name of the method which should be called from JavaScript. + /// In this case it's always the name of the -method. + /// + public string MethodName => nameof(Invoke); + + /// + /// Gets a reference to this object which is used to invoke the stored delegate from JavaScript. + /// + [JsonIgnore] // This property only has to be serialized by the JSRuntime where a custom converter will be used. + public DotNetObjectReference> HandlerReference { get; } + + /// + /// Gets a value indicating whether or not this delegate will return a value. + /// + public bool ReturnsValue => _delegateHasReturnValue; + + /// + /// Gets the indices of the ignored callback parameters. The parameters at these indices won't + /// be sent to C# and won't be deserialized. These indices are defined by the + /// s on the delegate passed to this instance. + /// + public IReadOnlyCollection IgnoredIndices { get; } + + static BitChartDelegateHandler() + { + // https://stackoverflow.com/a/429564/10883465 + var internalDelegateMethod = typeof(T).GetMethod("Invoke")!; + + _delegateParameters = internalDelegateMethod.GetParameters(); + _delegateHasReturnValue = internalDelegateMethod.ReturnType != typeof(void); + } + + /// + /// Creates a new instance of . + /// + /// The delegate you want to invoke from JavaScript. + public BitChartDelegateHandler(T function) + { + _function = function ?? throw new ArgumentNullException(nameof(function)); + ParameterInfo[] parameters = _function.GetMethodInfo().GetParameters(); + _ignoredIndices = new List(); + IgnoredIndices = new ReadOnlyCollection(_ignoredIndices); + for (int i = 0; i < parameters.Length; i++) + { + if (parameters[i].GetCustomAttribute(false) != null) + { + _ignoredIndices.Add(i); + } + } + + HandlerReference = DotNetObjectReference.Create(this); + } + + /// + /// Invokes the delegate dynamically. This method should only be called from JavaScript. + /// + /// + /// All the arguments for the method as array of json-strings. + /// This array can contain ANYTHING, do not trust its values. + /// + [JSInvokable] + public object? Invoke(params string[] jsonArgs) + { + if (_delegateParameters.Length != jsonArgs.Length) + throw new ArgumentException($"The function expects {_delegateParameters.Length} arguments but found {jsonArgs.Length}."); + + if (_delegateParameters.Length == 0) + return _function.DynamicInvoke(null); + + object?[] invokationArgs = new object[_delegateParameters.Length]; + for (int i = 0; i < _delegateParameters.Length; i++) + { + if (_ignoredIndices.Contains(i)) + continue; + + Type deserializeType = _delegateParameters[i].ParameterType; + if (deserializeType == typeof(object) || + typeof(JToken).IsAssignableFrom(deserializeType)) + { + invokationArgs[i] = JToken.Parse(jsonArgs[i]); + } + else + { +#if DEBUG + Console.WriteLine($"Deserializing: {jsonArgs[i]} to {deserializeType.Name}"); +#endif + invokationArgs[i] = JsonConvert.DeserializeObject(jsonArgs[i], deserializeType, BitChartJsInterop.JsonSerializerSettings); + } + } + + return _function.DynamicInvoke(invokationArgs); + } + + /// + public void Dispose() + { + HandlerReference.Dispose(); + GC.SuppressFinalize(this); + } + + /// + /// The method doesn't have any unmanaged resources to free BUT once this object is finalized + /// we need to prevent any further use of the to this object. Since the + /// will only be disposed if this instance is disposed or when dispose is called from JavaScript + /// (which shouldn't happen) we HAVE to dispose the reference when this instance is finalized. + /// + ~BitChartDelegateHandler() + { + Dispose(); + } + + /// + /// Converts a delegate of type to a implicitly. + /// + /// + public static implicit operator BitChartDelegateHandler(T function) => new BitChartDelegateHandler(function); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/JsInterop/BitChartIgnoreCallbackValueAttribute.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/JsInterop/BitChartIgnoreCallbackValueAttribute.cs new file mode 100644 index 0000000000..ec61298718 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/JsInterop/BitChartIgnoreCallbackValueAttribute.cs @@ -0,0 +1,18 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Defines that a callback parameter will be set to its default value instead of the deserialized +/// Chart.js value. Can be applied to parameters of methods passed to a . +/// Use this attribute to improve performance by applying it to parameters that are expected to have +/// a large size like the 'chartData' parameter of . +/// +[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] +public sealed class BitChartIgnoreCallbackValueAttribute : Attribute +{ + /// + /// Creates a new instance of the class. + /// + public BitChartIgnoreCallbackValueAttribute() + { + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/JsInterop/BitChartJavascriptHandler.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/JsInterop/BitChartJavascriptHandler.cs new file mode 100644 index 0000000000..e153e405b1 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/JsInterop/BitChartJavascriptHandler.cs @@ -0,0 +1,42 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a JavaScript function with a certain signature which will be called internally by JavaScript. +/// The type parameter defines the signature for the JavaScript method but doesn't have any actual use other than that. +/// +/// The signature of the method you want to invoke from JavaScript. This type parameter is +/// nothing more than a convention which tells you what parameters and return type is expected from the JavaScript function. +/// Just like you might mistype the method name, you might return the wrong value from JavaScript. These cases can't be caught +/// by the compiler so make sure to double-check those. +public class BitChartJavascriptHandler : IBitChartMethodHandler + where T : Delegate +{ + /// + /// Gets the namespace and name of the JavaScript function to be called, separated by a point. + /// E.g. "SampleFunctions.ItemHoverHandler" + /// Note 1: You must create this function in a JS file in wwwroot and reference it in index.html / _Host.cshtml. + /// Note 2: Make sure the function has the expected parameters and return type. See for the expected signature. + /// + public string MethodName { get; } + + /// + /// Creates a new instance of . + /// + /// The namespace and name of a JavaScript function (see for details). + public BitChartJavascriptHandler(string methodName) + { + if (methodName.HasNoValue()) + throw new ArgumentException("The method name cannot be null or whitespace. It has to include the namespace and name of the js-function."); + + if (methodName.Length < 3 || methodName.Count(c => c == '.') > 1) + throw new ArgumentException("The method name has to contain the namespace and name of the js-function separated by a single point."); + + MethodName = methodName; + } + + /// + /// Converts a string to a implicitly. + /// + /// The namespace and name of a JavaScript function to be called, separated by a point (see for details). + public static implicit operator BitChartJavascriptHandler(string methodName) => new BitChartJavascriptHandler(methodName); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/JsInterop/BitChartJsInterop.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/JsInterop/BitChartJsInterop.cs new file mode 100644 index 0000000000..332a689a1a --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/JsInterop/BitChartJsInterop.cs @@ -0,0 +1,195 @@ +using System.Dynamic; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; + +namespace Bit.BlazorUI.Legacy; + +/// +/// Interop layer from C# to JavaScript. +/// +internal static class BitChartJsInterop +{ + internal static JsonSerializerSettings JsonSerializerSettings { get; } = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new IgnoreDatasetCountContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy(true, false) + }, + Converters = { new IsoDateTimeConverter() } + }; + + public static ValueTask BitChartJsRemoveChart(this IJSRuntime jsRuntime, string? canvasId) + { + return jsRuntime.InvokeVoid("BitBlazorUI.Legacy.BitChart.removeChart", canvasId); + } + + /// + /// Set up a new chart. Call only once. + /// + /// + /// The config for the new chart. + /// + public static ValueTask BitChartJsSetupChart(this IJSRuntime jsRuntime, BitChartConfigBase chartConfig) + { + var dynParam = StripNulls(chartConfig); + Dictionary param = ConvertExpandoObjectToDictionary(dynParam!); + return jsRuntime.Invoke("BitBlazorUI.Legacy.BitChart.setupChart", param); + } + + /// + /// Update an existing chart. Make sure that the Chart with this already exists. + /// + /// + /// The updated config of the chart you want to update. + /// + public static ValueTask BitChartJsUpdateChart(this IJSRuntime jsRuntime, BitChartConfigBase chartConfig) + { + var dynParam = StripNulls(chartConfig); + var param = ConvertExpandoObjectToDictionary(dynParam!); + return jsRuntime.Invoke("BitBlazorUI.Legacy.BitChart.updateChart", param); + } + + + + /// + /// This method is specifically used to convert an with a Tree structure to a Dictionary<string, object>. + /// + /// The to convert. + /// The fully converted . + private static Dictionary ConvertExpandoObjectToDictionary(ExpandoObject expando) => RecursivelyConvertIDictToDict(expando!); + + /// + /// This method takes an IDictionary<string, object> and recursively converts it to a Dictionary<string, object>. + /// The idea is that every IDictionary<string, object> in the tree will be of type Dictionary<string, object> instead of some other implementation like . + /// + /// The IDictionary<string, object> to convert + /// The fully converted Dictionary<string, object> + private static Dictionary RecursivelyConvertIDictToDict(IDictionary value) => + value.ToDictionary( + keySelector => keySelector.Key, + elementSelector => + { + // if it's another IDict just go through it recursively + if (elementSelector.Value is IDictionary dict) + { + return RecursivelyConvertIDictToDict(dict); + } + + // if it's an IEnumerable check each element + if (elementSelector.Value is IEnumerable list) + { + // go through all objects in the list + // if the object is an IDict -> convert it + // if not keep it as is + return list + .Select(o => o is IDictionary dictionary + ? RecursivelyConvertIDictToDict(dictionary) + : o + ); + } + + // neither an IDict nor an IEnumerable -> it's fine to just return the value it has + return elementSelector.Value; + } + ); + + /// + /// Returns an object that is equivalent to the given parameter but without any null members AND it preserves s intact. + /// Preserving members is important because they might be instances which contain + /// delegates that can't be (de)serialized. + /// Stripping null members is only needed because chartJs doesn't handle null values and undefined values the same and with JSRuntime null gets + /// serialized to null instead of undefined (not at all) and WE CAN'T CHANGE THAT (see https://github.com/aspnet/AspNetCore/issues/12685). + /// If this were not the case, no null member stripping were necessary -> no json.net serialize-deserialize magic -> no loss of + /// instances -> no recovery of those. Everything would be better with AspNetCore#12685 finally being implemented but to fully migrate to System.Text.Json + /// we might also need corefx#38650 and corefx#39905. + /// Nevertheless, The Show must go on! + /// + /// The config you want to strip of null members. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + private static ExpandoObject? StripNulls(BitChartConfigBase chartConfig) + { + // Serializing with the custom serializer settings remove null members + string cleanChartConfigStr = JsonConvert.SerializeObject(chartConfig, JsonSerializerSettings); + + // Get back an ExpandoObject dynamic with the clean config - having an ExpandoObject allows us to add/replace members regardless of type + var cleanChartConfig = JsonConvert.DeserializeObject(cleanChartConfigStr, new ExpandoObjectConverter()); + + // Restore any .net refs that need to be passed intact + // TODO Find a way to do this dynamically. Maybe with attributes or something like that? + dynamic dynamicChartConfig = chartConfig; + if (dynamicChartConfig?.Options?.OnClick is IBitChartMethodHandler chartOnClick) + { + cleanChartConfig?.SetValue(path: "options.onClick", chartOnClick); + } + + if (dynamicChartConfig?.Options?.OnHover is IBitChartMethodHandler chartOnHover) + { + cleanChartConfig?.SetValue(path: "options.onHover", chartOnHover); + } + + if (dynamicChartConfig?.Options?.Legend?.OnClick is IBitChartMethodHandler legendOnClick) + { + cleanChartConfig?.SetValue(path: "options.legend.onClick", legendOnClick); + } + + if (dynamicChartConfig?.Options?.Legend?.OnHover is IBitChartMethodHandler legendOnHover) + { + cleanChartConfig?.SetValue(path: "options.legend.onHover", legendOnHover); + } + + if (dynamicChartConfig?.Options?.Legend?.Labels?.GenerateLabels is IBitChartMethodHandler generateLabels) + { + cleanChartConfig?.SetValue(path: "options.legend.labels.generateLabels", generateLabels); + } + + if (dynamicChartConfig?.Options?.Legend?.Labels?.Filter is IBitChartMethodHandler filter) + { + cleanChartConfig?.SetValue(path: "options.legend.labels.filter", filter); + } + + // Ticks callback need special handling because it can be either a single scale or two arrays of scales (xAxes and yAxes) + // it's really ugly (and quite slow), I hope we can improve this later on. Also ms, PLEASE, give us customizable jsruntime serialization. + try + { + if (dynamicChartConfig?.Options?.Scale?.Callback is IBitChartMethodHandler singleScaleTickCallback) + { + cleanChartConfig?.SetValue(path: "options.scale.callback", singleScaleTickCallback); + } + } + catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException) // happens when the options don't have a Scale property + { + try + { + // here we trust that if the Scales property exists, it contains an XAxes and a YAxes property + if (dynamicChartConfig?.Options?.Scales?.XAxes is IEnumerable xAxes) + { + AssignAxes(xAxes, "options.scales.xAxes"); + } + + if (dynamicChartConfig?.Options?.Scales?.YAxes is IEnumerable yAxes) + { + AssignAxes(yAxes, "options.scales.yAxes"); + } + } + catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException) { } // happens when the options don't have a Scales property (only Pie & Doughnut) + } + + return cleanChartConfig; + + void AssignAxes(IEnumerable axes, string axesPath) + { + var axesInDynamic = cleanChartConfig?.GetValue(axesPath) as IEnumerable; + + foreach ((object axis, ExpandoObject axisInDynamic) in axes.Zip(axesInDynamic!, (axis, axisInDynamic) => (axis, (ExpandoObject)axisInDynamic))) + { + if (((dynamic)axis)?.Ticks?.Callback is IBitChartMethodHandler axisTickCallback) + { + axisInDynamic.SetValue("ticks.callback", axisTickCallback); + } + } + } + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/JsInterop/IBitChartMethodHandler.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/JsInterop/IBitChartMethodHandler.cs new file mode 100644 index 0000000000..ac711aceae --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/JsInterop/IBitChartMethodHandler.cs @@ -0,0 +1,27 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a type that's able to handle method calls coming from JavaScript. +/// +/// The signature of the method this instance represents. +/// It may be an actual object or just a convention for the programmer. +public interface IBitChartMethodHandler : IBitChartMethodHandler + where T : Delegate +{ +} + +/// +/// Represents a type that's able to handle method calls coming from JavaScript. +/// In order to maintain the strongly typed nature of C#, please prefer using . +/// +//[Obsolete("Use " + nameof(IBitChartMethodHandler) + " instead.")] +public interface IBitChartMethodHandler +{ + /// + /// The name of the method which should be called from JavaScript. + /// In the case of , this is a reference + /// to a JavaScript namespace + function. In the case of , + /// this is the name of the delegate to be invoked by blazor. + /// + public string MethodName { get; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/LineChart/BitChartLineConfig.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/LineChart/BitChartLineConfig.cs new file mode 100644 index 0000000000..c04567a54c --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/LineChart/BitChartLineConfig.cs @@ -0,0 +1,12 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents the config for a line chart. +/// +public class BitChartLineConfig : BitChartConfigBase +{ + /// + /// Creates a new instance of . + /// + public BitChartLineConfig() : base(BitChartChartType.Line) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/LineChart/BitChartLineDataset.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/LineChart/BitChartLineDataset.cs new file mode 100644 index 0000000000..05894560a0 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/LineChart/BitChartLineDataset.cs @@ -0,0 +1,236 @@ +using Newtonsoft.Json; + +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a dataset for a line chart. +/// As per documentation here (Chart.js). +/// +/// The type of data this contains. +public class BitChartLineDataset : BitChartDataset +{ + /// + /// Creates a new instance of . + /// + public BitChartLineDataset() : base(BitChartChartType.Line) { } + + /// + /// Creates a new instance of with initial data. + /// + public BitChartLineDataset(IEnumerable data) : this() + { + AddRange(data); + } + + /// + /// Creates a new instance of with + /// a custom . Use this constructor when + /// you implement a line-like chart. + /// + /// The to use instead of . + protected BitChartLineDataset(BitChartChartType type) : base(type) { } + + /// + /// Gets or sets the fill color under the line. + /// See for working with colors. + /// + public string? BackgroundColor { get; set; } + + /// + /// Gets or sets the cap style of the line. + /// + public BitChartBorderCapStyle? BorderCapStyle { get; set; } + + /// + /// Gets or sets the color of the line. + /// See for working with colors. + /// + public string? BorderColor { get; set; } + + /// + /// Gets or sets the length and spacing of the line dashes. + /// As per documentation here (MDN). + /// + public int[]? BorderDash { get; set; } + + /// + /// Gets or sets the offset for the line dashes. + /// As per documentation here (MDN). + /// + public int? BorderDashOffset { get; set; } + + /// + /// Gets or sets the for the lines. + /// + public BitChartBorderJoinStyle? BorderJoinStyle { get; set; } + + /// + /// Gets or sets the width of the line (in pixels). + /// + public int? BorderWidth { get; set; } + + /// + /// Gets or sets the algorithm used to interpolate a smooth curve from the discrete data points. + /// + public BitChartCubicInterpolationMode? CubicInterpolationMode { get; set; } + + /// + /// Gets or sets how to clip relative to the chart area. Positive values allow overflow, + /// negative values clip that many pixels inside the chart area. + /// + public BitChartClipping? Clip { get; set; } + + /// + /// Gets or sets how to fill the area under the line. + /// + public BitChartFillingMode? Fill { get; set; } + + /// + /// Gets or sets the fill color under the line when hovered. + /// See for working with colors. + /// + public string? HoverBackgroundColor { get; set; } + + /// + /// Gets or sets the cap style of the line when hovered. + /// + public BitChartBorderCapStyle? HoverBorderCapStyle { get; set; } + + /// + /// Gets or sets the color of the line when hovered. + /// See for working with colors. + /// + public string? HoverBorderColor { get; set; } + + /// + /// Gets or sets the length and spacing of the line dashes when hovered. + /// As per documentation here (MDN). + /// + public int[]? HoverBorderDash { get; set; } + + /// + /// Gets or sets the offset for the line dashes when hovered. + /// As per documentation here (MDN). + /// + public int? HoverBorderDashOffset { get; set; } + + /// + /// Gets or sets the for the lines when hovered. + /// + public BitChartBorderJoinStyle? HoverBorderJoinStyle { get; set; } + + /// + /// Gets or sets the width of the line when hovered (in pixels). + /// + public int? HoverBorderWidth { get; set; } + + /// + /// Gets or sets the label for the dataset which appears in the legend and the tooltips. + /// + public string? Label { get; set; } + + /// + /// Gets or sets the bezier curve tension of the line. Set to 0 to draw straight lines. + /// This option is ignored if is used. + /// + public double? LineTension { get; set; } + + /// + /// Gets or sets the drawing order of this dataset. + /// Also affects the order for stacking, tooltips, and the legend. + /// + public int? Order { get; set; } + + /// + /// Gets or sets the fill color for the points. + /// See for working with colors. + /// + public BitChartIndexableOption? PointBackgroundColor { get; set; } + + /// + /// Gets or sets the border color for the points. + /// See for working with colors. + /// + public BitChartIndexableOption? PointBorderColor { get; set; } + + /// + /// Gets or sets the width of the point border (in pixels). + /// + public BitChartIndexableOption? PointBorderWidth { get; set; } + + /// + /// Gets or sets the radius of the non-displayed point that reacts to mouse events (in pixels). + /// + public BitChartIndexableOption? PointHitRadius { get; set; } + + /// + /// Gets or sets the fill color for the points when hovering. + /// See for working with colors. + /// + public BitChartIndexableOption? PointHoverBackgroundColor { get; set; } + + /// + /// Gets or sets the border color for the points when hovering. + /// See for working with colors. + /// + public BitChartIndexableOption? PointHoverBorderColor { get; set; } + + /// + /// Gets or sets the width of the point border when hovered (in pixels). + /// + public BitChartIndexableOption? PointHoverBorderWidth { get; set; } + + /// + /// Gets or sets the radius of the point when hovered. + /// + public BitChartIndexableOption? PointHoverRadius { get; set; } + + /// + /// Gets or sets the radius of the point shape. If set to 0, the point is not rendered. + /// + public BitChartIndexableOption? PointRadius { get; set; } + + /// + /// Gets or sets the rotation of the points in degrees. + /// + public BitChartIndexableOption? PointRotation { get; set; } + + /// + /// Gets or sets the for this dataset. + /// + public BitChartIndexableOption? PointStyle { get; set; } + + /// + /// Gets or sets a value indicating whether or not the line is drawn for this dataset. + /// + public bool? ShowLine { get; set; } + + /// + /// Gets or sets a value indicating whether or not lines will be drawn between points with no or null data. + /// If , points with NaN data will create a break in the line. + /// + public bool? SpanGaps { get; set; } + + /// + /// Gets or sets a value indicating whether or not the line is shown as a stepped line. + /// + /// If this value is set to anything other than , + /// will be ignored. + /// + /// + public BitChartSteppedLine? SteppedLine { get; set; } + + /// + /// Gets or sets the ID of the x axis to plot this dataset on. If not specified, + /// this defaults to the ID of the first found x axis. + /// + [JsonProperty("xAxisID")] + public string? XAxisId { get; set; } + + /// + /// Gets or sets the ID of the y axis to plot this dataset on. If not specified, + /// this defaults to the ID of the first found y axis. + /// + [JsonProperty("yAxisID")] + public string? YAxisId { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/LineChart/BitChartLineOptions.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/LineChart/BitChartLineOptions.cs new file mode 100644 index 0000000000..ec3d594586 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/LineChart/BitChartLineOptions.cs @@ -0,0 +1,22 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The options-subconfig of a +/// +public class BitChartLineOptions : BitChartBaseConfigOptions +{ + /// + /// The scales for this chart. You can use any for x and y. + /// + public BitChartScales? Scales { get; set; } + + /// + /// If false, the lines between points are not drawn. + /// + public bool? ShowLines { get; set; } + + /// + /// If false, NaN data causes a break in the line. + /// + public bool? SpanGaps { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/LineChart/BitChartScales.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/LineChart/BitChartScales.cs new file mode 100644 index 0000000000..8875f73ebe --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/LineChart/BitChartScales.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; + +namespace Bit.BlazorUI.Legacy; + +/// +/// Defines the scales for cartesian charts by holding the x and y axes. +/// +public class BitChartScales +{ + /// + /// Gets or sets the configurations for the x-axes. + /// + [JsonProperty("xAxes")] + public List? XAxes { get; set; } + + /// + /// Gets or sets the configurations for the y-axes. + /// + [JsonProperty("yAxes")] + public List? YAxes { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PieChart/BitChartPieConfig.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PieChart/BitChartPieConfig.cs new file mode 100644 index 0000000000..1158089a71 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PieChart/BitChartPieConfig.cs @@ -0,0 +1,18 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents the config for a pie chart. +/// +public class BitChartPieConfig : BitChartConfigBase +{ + /// + /// Creates a new instance of . + /// + /// + /// If , the chart-type will be set to . + /// If , the chart-type will be set to . + /// This parameter can generally be left on and only needs to be + /// adjusted when you register a plugin which only works for doughnut charts or something similar. + /// + public BitChartPieConfig(bool useDoughnutType = false) : base(useDoughnutType ? BitChartChartType.Doughnut : BitChartChartType.Pie) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PieChart/BitChartPieDataset.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PieChart/BitChartPieDataset.cs new file mode 100644 index 0000000000..678e02678c --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PieChart/BitChartPieDataset.cs @@ -0,0 +1,99 @@ +namespace Bit.BlazorUI.Legacy; + +/// +public class BitChartPieDataset : BitChartPieDataset +{ + /// + public BitChartPieDataset(bool useDoughnutDefaults = false) : base(useDoughnutDefaults) { } + + /// + public BitChartPieDataset(IEnumerable data, bool useDoughnutDefaults = false) : base(data, useDoughnutDefaults) { } + + /// + protected BitChartPieDataset(BitChartChartType type) : base(type) { } +} + +/// +/// Represents a dataset for a pie or doughnut chart. +/// As per documentation here (Chart.js). +/// +public class BitChartPieDataset : BitChartDataset +{ + /// + /// Creates a new instance of . + /// + /// + /// If , the dataset-type will be set to + /// which causes Chart.js to use the doughnut defaults. + /// If , the dataset-type will be set to . + /// Unless the defaults were changed manually, + /// the pie defaults are identical to the doughnut defaults and then this setting doesn't matter. + /// + public BitChartPieDataset(bool useDoughnutDefaults = false) : base(useDoughnutDefaults ? BitChartChartType.Doughnut : BitChartChartType.Pie) { } + + /// + /// Creates a new instance of with initial data. + /// + /// + public BitChartPieDataset(IEnumerable data, bool useDoughnutDefaults = false) : this(useDoughnutDefaults) + { + AddRange(data); + } + + /// + /// Creates a new instance of with + /// a custom . Use this constructor when + /// you implement a pie-like chart. + /// + /// The to use instead of . + protected BitChartPieDataset(BitChartChartType type) : base(type) { } + + /// + /// Gets or sets the background color of the arcs. + /// This property should usually be indexed, otherwise it's hard to distinguish the individual arcs. + /// See for working with colors. + /// + public BitChartIndexableOption? BackgroundColor { get; set; } + + /// + /// Gets or sets the border alignment. When is set, + /// the borders of arcs next to each other will overlap. When + /// is set, it is guaranteed that all the borders will not overlap. + /// + public BitChartIndexableOption? BorderAlign { get; set; } + + /// + /// Gets or sets the border color of the arcs. + /// See for working with colors. + /// + public BitChartIndexableOption? BorderColor { get; set; } + + /// + /// Gets or sets the border width of the arcs (in pixels). + /// + public BitChartIndexableOption? BorderWidth { get; set; } + + /// + /// Gets or sets the background color of the arcs when hovered. + /// See for working with colors. + /// + public BitChartIndexableOption? HoverBackgroundColor { get; set; } + + /// + /// Gets or sets the border color of the arcs when hovered. + /// See for working with colors. + /// + public BitChartIndexableOption? HoverBorderColor { get; set; } + + /// + /// Gets or sets the border width of the arcs when hovered (in pixels). + /// + public BitChartIndexableOption? HoverBorderWidth { get; set; } + + /// + /// Gets or sets the relative thickness of the dataset. + /// Providing a value for will cause the pie or doughnut dataset to be + /// drawn with a thickness relative to the sum of all the dataset weight values. + /// + public int? Weight { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PieChart/BitChartPieOptions.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PieChart/BitChartPieOptions.cs new file mode 100644 index 0000000000..bbdced3683 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PieChart/BitChartPieOptions.cs @@ -0,0 +1,28 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The options-subconfig of a . +/// +public class BitChartPieOptions : BitChartBaseConfigOptions +{ + /// + /// Gets or sets the percentage of the chart that is cut out of the middle. + /// Default for Pie is 0, Default for Doughnut is 50. This will be filled in by Chart.js unless you specify a non-null value. + /// + public int? CutoutPercentage { get; set; } + + /// + /// Gets or sets the animation-configuration for this chart. + /// + public new BitChartArcAnimation? Animation { get; set; } + + /// + /// Gets or sets the starting angle to draw arcs from. + /// + public double? Rotation { get; set; } + + /// + /// Gets or sets the sweep to allow arcs to cover. + /// + public double? Circumference { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PolarAreaChart/BitChartPolarAreaConfig.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PolarAreaChart/BitChartPolarAreaConfig.cs new file mode 100644 index 0000000000..e58408c75f --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PolarAreaChart/BitChartPolarAreaConfig.cs @@ -0,0 +1,12 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents the config for a polar area chart. +/// +public class BitChartPolarAreaConfig : BitChartConfigBase +{ + /// + /// Creates a new instance of . + /// + public BitChartPolarAreaConfig() : base(BitChartChartType.PolarArea) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PolarAreaChart/BitChartPolarAreaDataset.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PolarAreaChart/BitChartPolarAreaDataset.cs new file mode 100644 index 0000000000..feb52c3060 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PolarAreaChart/BitChartPolarAreaDataset.cs @@ -0,0 +1,64 @@ +namespace Bit.BlazorUI.Legacy; + +/// +public class BitChartPolarAreaDataset : BitChartPolarAreaDataset +{ + /// + public BitChartPolarAreaDataset() { } + + /// + public BitChartPolarAreaDataset(IEnumerable data) : base(data) { } + + /// + protected BitChartPolarAreaDataset(BitChartChartType type) : base(type) { } +} + +/// +/// Represents a dataset for a polar area chart. +/// As per documentation here (Chart.js). +/// +// Very similar to PieDataset, so the summaries are inherited. +public class BitChartPolarAreaDataset : BitChartDataset +{ + /// + /// Creates a new instance of . + /// + public BitChartPolarAreaDataset() : base(BitChartChartType.PolarArea) { } + + /// + /// Creates a new instance of with initial data. + /// + public BitChartPolarAreaDataset(IEnumerable data) : this() + { + AddRange(data); + } + + /// + /// Creates a new instance of with + /// a custom . Use this constructor when + /// you implement a polar-area-like chart. + /// + /// The to use instead of . + protected BitChartPolarAreaDataset(BitChartChartType type) : base(type) { } + + /// + public BitChartIndexableOption? BackgroundColor { get; set; } + + /// + public BitChartIndexableOption? BorderAlign { get; set; } + + /// + public BitChartIndexableOption? BorderColor { get; set; } + + /// + public BitChartIndexableOption? BorderWidth { get; set; } + + /// + public BitChartIndexableOption? HoverBackgroundColor { get; set; } + + /// + public BitChartIndexableOption? HoverBorderColor { get; set; } + + /// + public BitChartIndexableOption? HoverBorderWidth { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PolarAreaChart/BitChartPolarAreaOptions.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PolarAreaChart/BitChartPolarAreaOptions.cs new file mode 100644 index 0000000000..ebc5078249 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/PolarAreaChart/BitChartPolarAreaOptions.cs @@ -0,0 +1,22 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// The options-subconfig of a . +/// +public class BitChartPolarAreaOptions : BitChartBaseConfigOptions +{ + /// + /// Gets or sets the starting angle to draw arcs for the first item in a dataset. + /// + public double? StartAngle { get; set; } + + /// + /// Gets or sets the animation-configuration for this chart. + /// + public new BitChartArcAnimation? Animation { get; set; } + + /// + /// The scale (axis) for this chart. + /// + public BitChartLinearRadialAxis? Scale { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/RadarChart/BitChartRadarConfig.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/RadarChart/BitChartRadarConfig.cs new file mode 100644 index 0000000000..f3ac15d75c --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/RadarChart/BitChartRadarConfig.cs @@ -0,0 +1,12 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents the config for a radar chart. +/// +public class BitChartRadarConfig : BitChartConfigBase +{ + /// + /// Creates a new instance of . + /// + public BitChartRadarConfig() : base(BitChartChartType.Radar) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/RadarChart/BitChartRadarDataset.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/RadarChart/BitChartRadarDataset.cs new file mode 100644 index 0000000000..986f6f455e --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/RadarChart/BitChartRadarDataset.cs @@ -0,0 +1,135 @@ +namespace Bit.BlazorUI.Legacy; + +/// +public class BitChartRadarDataset : BitChartRadarDataset +{ + /// + public BitChartRadarDataset() { } + + /// + public BitChartRadarDataset(IEnumerable data) : base(data) { } + + /// + protected BitChartRadarDataset(BitChartChartType type) : base(type) { } +} + +/// +/// Represents a dataset for a radar chart. +/// As per documentation here (Chart.js). +/// +// Very similar to LineDataset, so the summaries are inherited. +public class BitChartRadarDataset : BitChartDataset +{ + /// + /// Creates a new instance of . + /// + public BitChartRadarDataset() : base(BitChartChartType.Radar) { } + + /// + /// Creates a new instance of with initial data. + /// + public BitChartRadarDataset(IEnumerable data) : this() + { + AddRange(data); + } + + /// + /// Creates a new instance of with + /// a custom . Use this constructor when + /// you implement a radar-like chart. + /// + /// The to use instead of . + protected BitChartRadarDataset(BitChartChartType type) : base(type) { } + + /// + public string? BackgroundColor { get; set; } + + /// + public BitChartBorderCapStyle? BorderCapStyle { get; set; } + + /// + public string? BorderColor { get; set; } + + /// + public int[]? BorderDash { get; set; } + + /// + public int? BorderDashOffset { get; set; } + + /// + public BitChartBorderJoinStyle? BorderJoinStyle { get; set; } + + /// + public int? BorderWidth { get; set; } + + /// + public BitChartFillingMode? Fill { get; set; } + + /// + public string? HoverBackgroundColor { get; set; } + + /// + public BitChartBorderCapStyle? HoverBorderCapStyle { get; set; } + + /// + public string? HoverBorderColor { get; set; } + + /// + public int[]? HoverBorderDash { get; set; } + + /// + public int? HoverBorderDashOffset { get; set; } + + /// + public BitChartBorderJoinStyle? HoverBorderJoinStyle { get; set; } + + /// + public int? HoverBorderWidth { get; set; } + + /// + public string? Label { get; set; } + + /// + /// Gets or sets the bezier curve tension of the line. Set to 0 to draw straight lines. + /// + public double? LineTension { get; set; } + + /// + public int? Order { get; set; } + + /// + public BitChartIndexableOption? PointBackgroundColor { get; set; } + + /// + public BitChartIndexableOption? PointBorderColor { get; set; } + + /// + public BitChartIndexableOption? PointBorderWidth { get; set; } + + /// + public BitChartIndexableOption? PointHitRadius { get; set; } + + /// + public BitChartIndexableOption? PointHoverBackgroundColor { get; set; } + + /// + public BitChartIndexableOption? PointHoverBorderColor { get; set; } + + /// + public BitChartIndexableOption? PointHoverBorderWidth { get; set; } + + /// + public BitChartIndexableOption? PointHoverRadius { get; set; } + + /// + public BitChartIndexableOption? PointRadius { get; set; } + + /// + public BitChartIndexableOption? PointRotation { get; set; } + + /// + public BitChartIndexableOption? PointStyle { get; set; } + + /// + public bool? SpanGaps { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/RadarChart/BitChartRadarOptions.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/RadarChart/BitChartRadarOptions.cs new file mode 100644 index 0000000000..8991c75fd3 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/RadarChart/BitChartRadarOptions.cs @@ -0,0 +1,18 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents the options-subconfig of a . +/// +public class BitChartRadarOptions : BitChartBaseConfigOptions +{ + /// + /// Gets or sets the scale configuration for this chart. + /// + public BitChartLinearRadialAxis? Scale { get; set; } + + /// + /// Gets or sets a value indicating whether or not line gaps (by NaN data) will be spanned. + /// If , NaN data causes a break in the line. + /// + public bool? SpanGaps { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/ScatterChart/BitChartScatterConfig.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/ScatterChart/BitChartScatterConfig.cs new file mode 100644 index 0000000000..5d43b2c2ca --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/ScatterChart/BitChartScatterConfig.cs @@ -0,0 +1,17 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents the config for a scatter chart. +/// +/// Scatter charts are based on basic line charts with the x axis changed to a +/// (unless otherwise specified). +/// Therefore, many configuration options are from the line chart. +/// +/// +public class BitChartScatterConfig : BitChartConfigBase +{ + /// + /// Creates a new instance of . + /// + public BitChartScatterConfig() : base(BitChartChartType.Scatter) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/ScatterChart/BitChartScatterDataset.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/ScatterChart/BitChartScatterDataset.cs new file mode 100644 index 0000000000..6c99a7a87f --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/ScatterChart/BitChartScatterDataset.cs @@ -0,0 +1,29 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a dataset for a scatter chart. +/// As per documentation here (Chart.js). +/// +public class BitChartScatterDataset : BitChartLineDataset +{ + /// + /// Creates a new instance of . + /// + public BitChartScatterDataset() : base(BitChartChartType.Scatter) { } + + /// + /// Creates a new instance of with initial data. + /// + public BitChartScatterDataset(IEnumerable data) : base(BitChartChartType.Scatter) + { + AddRange(data); + } + + /// + /// Creates a new instance of with + /// a custom . Use this constructor when + /// you implement a scatter-like chart. + /// + /// The to use instead of . + protected BitChartScatterDataset(BitChartChartType type) : base(type) { } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Util/BitChartColorUtil.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Util/BitChartColorUtil.cs new file mode 100644 index 0000000000..17a3196467 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Util/BitChartColorUtil.cs @@ -0,0 +1,75 @@ +using System.Globalization; + +namespace Bit.BlazorUI.Legacy; + +/// +/// Provides useful methods for working with colors. Particularly the conversion from different kinds of C#-colors to string colors which are understood by javascript/css. +/// +public static class BitChartColorUtil +{ + private static readonly Random _rand = new Random(); + + /// + /// Produces a string in the form '#rrggbb' with the provided rgb values. + /// + /// + /// + /// + public static string ColorHexString(byte r, byte g, byte b) + { + return $"#{r:X2}{g:X2}{b:X2}"; + } + + /// + /// Produces a string in the form 'rgba(r, g, b, 1)' with the provided rgb values where the alpha is fixed at 1. + /// + /// + /// + /// + public static string ColorString(byte r, byte g, byte b) => ColorString(r, g, b, 1); + + /// + /// Produces a string in the form 'rgba(r, g, b, alpha)' with the provided rgb and alpha values. + /// + /// + /// + /// + /// + public static string ColorString(byte r, byte g, byte b, double alpha) + { + return $"rgba({r}, {g}, {b}, {alpha.ToString(CultureInfo.InvariantCulture)})"; + } + + /// + /// Produces a string of the form 'rgba(r, g, b, alpha)' with random values for rgb and alpha. + /// + public static string RandomColorString() + { + byte[] rgb = new byte[3]; + double alpha; + + lock (_rand) + { + _rand.NextBytes(rgb); + alpha = _rand.NextDouble(); + } + + return ColorString(rgb[0], rgb[1], rgb[2], alpha); + } + + /// + /// Generates the corresponding string representation of a object. + /// Depending on the value, it's returned as hex string or as rgba string. + /// + public static string FromDrawingColor(System.Drawing.Color color) + { + if (color.A != byte.MaxValue) + { + return ColorString(color.R, color.G, color.B, (double)color.A / byte.MaxValue); + } + else + { + return ColorHexString(color.R, color.G, color.B); + } + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Util/ExpandoObjectExtensions.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Util/ExpandoObjectExtensions.cs new file mode 100644 index 0000000000..5386ca5414 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/Chart/Util/ExpandoObjectExtensions.cs @@ -0,0 +1,148 @@ +using System.Dynamic; + +namespace Bit.BlazorUI.Legacy; + +/// +/// This class contains extensions for the type which allows for easier access and modification particularly when nested. +/// +internal static class ExpandoObjectExtensions +{ + /// + /// Ensures that a specified path (e.g. "A.B.C") exists on the . Any missing members will be filled with empty s. + /// The is modified and also returned for convenience. If parts of the path already exist, the function attempts to add the missing members. + /// If a part of the path (excluding the very last part) doesn't support adding members (doesn't implement IDictionary<string, object>), an exception is thrown. + /// + /// The to modify. + /// The path you want to ensure exists. This can be something like "A.B.C" which would be the same as accessing value.A.B.C. + /// If true, allows the last part of the path to not implement IDictionary<string, object> and be of a different type (e.g. string). + /// If you just want to see if the path exists, leave it on true. If you want to make sure you can add members to the last part, set it to false. + /// + public static ExpandoObject EnsurePathExists(this ExpandoObject value, string path, bool allowForeignLastPart = true) + { + value ??= new ExpandoObject(); + + if (path.HasNoValue()) return value; + + string[] segments = path.Split('.'); + IDictionary source = value!; + for (int i = 0; i < segments.Length; i++) + { + string segment = segments[i]; + IDictionary? newSource; + if (source.TryGetValue(segment, out object? val)) + { + newSource = val as IDictionary; + } + else + { + newSource = new ExpandoObject() as IDictionary; + source[segment] = newSource; + } + + if (i < segments.Length - 1 || !allowForeignLastPart) + { + source = newSource ?? throw new Exception($"The object at path '{string.Join(".", segments.Take(i + 1))}' is already set to a value of type '{source[segment].GetType()}' which doesn't support adding new members."); + } + } + + return value; + } + + /// + /// Checks if a path exists without modifying the . Since the path has to be followed (enumerated) + /// to check this, you can retrieve all the values using the out parameter . + /// + /// The on which you want to check a path. + /// The path to check. + /// All the values from the path; same as using value.EnumeratePath(path).ToArray(). + /// + public static bool PathExists(this ExpandoObject value, string path, out object[] values) + { + values = value.EnumeratePath(path).ToArray(); + return path.Count(c => c == '.') + 1 == values.Length; + } + + /// + /// Checks if a path exists without modifying the . + /// + /// The on which you want to check a path. + /// The path to check. + /// + public static bool PathExists(this ExpandoObject value, string path) => PathExists(value, path, out _); + + /// + /// Enumerates the objects in a path of the . For a "a.b.c" the iteration + /// would be value.a, value.a.b, value.a.b.c (given that all three values exist). If a part of the path doesn't exist + /// or can't be accessed because the parent doesn't implement IDictionary<string, object>, the enumeration stops. + /// + /// The whose values you want to iterate over. + /// The path you want to follow. + /// + public static IEnumerable EnumeratePath(this ExpandoObject value, string path) + { + if (value is null) + throw new ArgumentNullException(nameof(value)); + + if (path.HasNoValue()) + throw new ArgumentException("The path cannot be null or whitespace."); + + string[] segments = path.Split('.'); + IDictionary source = value!; + foreach (string segment in segments) + { + object? x = null; + try + { + x = source![segment]; + } + catch (Exception e) when (e is KeyNotFoundException || e is NullReferenceException) + { + // x = null but that's already done + } + + if (x is null) yield break; + source = (x as IDictionary)!; + + yield return x; + } + } + + /// + /// Enumerates the path and returns the last element. If the path can't be followed (entirely), an exception is thrown. + /// + /// The whose value you want to retrieve. + /// The path of the value you want to get. + /// + public static object GetValue(this ExpandoObject value, string path) + { + if (!value.PathExists(path, out object[] values)) + throw new ArgumentException($"The path '{path}' could not be followed (entirely). {values.Length} parts of the path were followed."); + + return values[^1]; + } + + /// + /// Sets a value in a certain place (path) of the . The is modified and also returned for convenience. + /// This method calls to ensure the path exists. + /// + /// The whose value you want to set. + /// The path leading to the value you want to set. + /// The actual value you want to set. + /// + public static ExpandoObject SetValue(this ExpandoObject expando, string path, object value) + { + if (path.HasNoValue()) + throw new ArgumentException("The path cannot be null or whitespace."); + + string[] segments = path.Split('.'); + IDictionary source = expando.EnsurePathExists(path)!; + for (int i = 0; i < segments.Length - 1; i++) + { + source = (source[segments[i]] as IDictionary)!; + } + + source[segments[^1]] = value; + + return expando; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGrid.razor b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGrid.razor new file mode 100644 index 0000000000..d2450b201a --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGrid.razor @@ -0,0 +1,152 @@ +@namespace Bit.BlazorUI.Legacy +@typeparam TGridItem + + + @{ + StartCollectingColumns(); + } + @(Columns ?? ChildContent) + + @{ + FinishCollectingColumns(); + } + + + + + + @_renderColumnHeaders + + + + @if (Virtualize) + { + if (IsLoading && LoadingTemplate is not null) + { + + + + } + else + { + + } + } + else + { + if (IsLoading && LoadingTemplate is not null) + { + + + + } + else + { + @_renderNonVirtualizedRows + } + } + +
+ @LoadingTemplate +
+ @LoadingTemplate +
+
+
+ +@code { + private void RenderNonVirtualizedRows(RenderTreeBuilder __builder) + { + var initialRowIndex = 2; // aria-rowindex is 1-based, plus the first row is the header + var rowIndex = initialRowIndex; + foreach (var item in _currentNonVirtualizedViewItems) + { + RenderRow(__builder, rowIndex++, item); + } + + // When pagination is enabled, by default ensure we render the exact number of expected rows per page, + // even if there aren't enough data items. This avoids the layout jumping on the last page. + // Consider making this optional. + if (Pagination is not null) + { + while (rowIndex++ < initialRowIndex + Pagination.ItemsPerPage) + { + + + + } + } + } + + private void RenderRow(RenderTreeBuilder __builder, int rowIndex, TGridItem item) + { + if (RowTemplate is null) + { + RenderOriginalRow(__builder, rowIndex, item); + } + else + { + var args = new BitDataGridRowTemplateArgs + { + RowIndex = rowIndex, + RowItem = item, + OriginalRow = (builder) => RenderOriginalRow(builder, rowIndex, item) + }; + __builder.AddContent(0, RowTemplate(args)); + } + } + + private void RenderOriginalRow(RenderTreeBuilder __builder, int rowIndex, TGridItem item) + { + + @foreach (var col in _columns) + { + + @{ + col.CellContent(__builder, item); + } + + } + + } + + private void RenderPlaceholderRow(RenderTreeBuilder __builder, PlaceholderContext placeholderContext) + { + + @foreach (var col in _columns) + { + + @{ + col.RenderPlaceholderContent(__builder, placeholderContext); + } + + } + + } + + private void RenderColumnHeaders(RenderTreeBuilder __builder) + { + foreach (var col in _columns) + { + +
@col.HeaderContent
+ + @if (col == _displayOptionsForColumn) + { +
@col.ColumnOptions
+ } + + @if (ResizableColumns) + { +
+ } + + } + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGrid.razor.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGrid.razor.cs new file mode 100644 index 0000000000..db12f8b63e --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGrid.razor.cs @@ -0,0 +1,751 @@ +// a fork from the Blazor QuickGrid at https://github.com/dotnet/aspnetcore/tree/main/src/Components/QuickGrid + +namespace Bit.BlazorUI.Legacy; + +/// +/// BitDataGrid is a robust way to display an information-rich collection of items, and allow people to sort, and filter the content. +/// +/// The type of data represented by each row in the grid. +[CascadingTypeParameter(nameof(TGridItem))] +public partial class BitDataGrid : IAsyncDisposable +{ + private bool _disposed; + private int _ariaBodyRowCount; + private ElementReference _tableReference; + private Virtualize<(int, TGridItem)>? _virtualizeComponent; + private ICollection _currentNonVirtualizedViewItems = Array.Empty(); + + // IQueryable only exposes synchronous query APIs. IAsyncQueryExecutor is an adapter that lets us invoke any + // async query APIs that might be available. We have built-in support for using EF Core's async query APIs. + private IAsyncQueryExecutor? _asyncQueryExecutor; + + // We cascade the InternalGridContext to descendants, which in turn call it to add themselves to _columns + // This happens on every render so that the column list can be updated dynamically + private InternalGridContext _internalGridContext; + private List> _columns; + private bool _collectingColumns; // Columns might re-render themselves arbitrarily. We only want to capture them at a defined time. + + // Tracking state for options and sorting + private BitDataGridColumnBase? _displayOptionsForColumn; + private BitDataGridColumnBase? _sortByColumn; + private bool _sortByAscending; + private bool _checkColumnOptionsPosition; + // Set when column recollection drops the active sort column; triggers a data refresh after render + // so the grid query stays in sync with the (now changed) header sort state. + private bool _queueSortReconciliationRefresh; + // Tracks whether columns have been collected at least once, and the sort column captured at the + // start of a collection pass, so a *new* default sort applied during a later recollection can also + // queue a refresh (the very first collection already loads data via ColumnsFirstCollected). + private bool _columnsCollectedOnce; + private BitDataGridColumnBase? _sortByColumnBeforeCollect; + // Captures the first default-sort column (and its direction) discovered during the current + // collection pass, so FinishCollectingColumns can adopt it when the previously active sort column + // is no longer present after recollection instead of clearing sorting outright. + private BitDataGridColumnBase? _defaultSortColumnDuringCollect; + private BitDataGridSortDirection? _defaultSortDirectionDuringCollect; + + // The associated ES6 module, which uses document-level event listeners + //private IJSObjectReference? _jsModule; + private IJSObjectReference? _jsEventDisposable; + + // Caches of method->delegate conversions + private readonly RenderFragment _renderColumnHeaders; + private readonly RenderFragment _renderNonVirtualizedRows; + + // We try to minimize the number of times we query the items provider, since queries may be expensive + // We only re-query when the developer calls RefreshDataAsync, or if we know something's changed, such + // as sort order, the pagination state, or the data source itself. These fields help us detect when + // things have changed, and to discard earlier load attempts that were superseded. + private int? _lastRefreshedPaginationStateHash; + private object? _lastAssignedItemsOrProvider; + // Tracks the Virtualize value the data was last refreshed under, so a flip between virtualized and + // non-virtualized rendering forces a re-query (otherwise the stale non-virtualized view would linger). + private bool? _lastRefreshedVirtualize; + private CancellationTokenSource? _pendingDataLoadCancellationTokenSource; + // Hash of the collected column set the resize handles were last bound against, so we only rebind + // when the columns actually change rather than on every render. + private int? _lastInitColumnsHash; + // Tracks the ResizableColumns value the resize handles were last bound against, so toggling the + // feature on/off (without otherwise changing the columns) still rebinds the new/removed handles. + private bool _lastResizableColumns; + + // If the PaginationState mutates, it raises this event. We use it to trigger a re-render. + private readonly EventCallbackSubscriber _currentPageItemsChanged; + + + + [Inject] private IJSRuntime _js { get; set; } = default!; + [Inject] private IServiceProvider _services { get; set; } = default!; + + + + /// + /// Constructs an instance of . + /// + public BitDataGrid() + { + _columns = new(); + _internalGridContext = new(this); + _currentPageItemsChanged = new(EventCallback.Factory.Create(this, RefreshDataCoreAsync)); + _renderColumnHeaders = RenderColumnHeaders; + _renderNonVirtualizedRows = RenderNonVirtualizedRows; + + // As a special case, we don't issue the first data load request until we've collected the initial set of columns + // This is so we can apply default sort order (or any future per-column options) before loading data + // We use EventCallbackSubscriber to safely hook this async operation into the synchronous rendering flow + var columnsFirstCollectedSubscriber = new EventCallbackSubscriber( + EventCallback.Factory.Create(this, RefreshDataCoreAsync)); + columnsFirstCollectedSubscriber.SubscribeOrMove(_internalGridContext.ColumnsFirstCollected); + } + + private bool IsLoading => _pendingDataLoadCancellationTokenSource is not null; + + + + /// + /// Defines the child components of this instance. For example, you may define columns by adding + /// components derived from the base class. + /// + [Parameter] public RenderFragment? ChildContent { get; set; } + + /// + /// An optional CSS class name. If given, this will be included in the class attribute of the rendered table. + /// + [Parameter] public string? Class { get; set; } + + /// + /// Alias of the ChildContent parameter. + /// + [Parameter] public RenderFragment? Columns { get; set; } + + /// + /// Optionally defines a value for @key on each rendered row. Typically this should be used to specify a + /// unique identifier, such as a primary key value, for each data item. + /// + /// This allows the grid to preserve the association between row elements and data items based on their + /// unique identifiers, even when the TGridItem instances are replaced by new copies (for + /// example, after a new query against the underlying data store). + /// + /// If not set, the @key will be the TGridItem instance itself. + /// + [Parameter] public Func ItemKey { get; set; } = x => x!; + + /// + /// A queryable source of data for the grid. + /// + /// This could be in-memory data converted to queryable using the + /// extension method, + /// or an EntityFramework DataSet or an derived from it. + /// + /// You should supply either or , but not both. + /// + [Parameter] public IQueryable? Items { get; set; } + + /// + /// This is applicable only when using . It defines an expected height in pixels for + /// each row, allowing the virtualization mechanism to fetch the correct number of items to match the display + /// size and to ensure accurate scrolling. + /// + [Parameter] public float ItemSize { get; set; } = 50; + + /// + /// A callback that supplies data for the grid. + /// + /// You should supply either or , but not both. + /// + [Parameter] public BitDataGridItemsProvider? ItemsProvider { get; set; } + + /// + /// The custom template to render while loading the new items. + /// It is honored in both the non-virtualized and the virtualized () paths: + /// while a data refresh is in flight the template is shown instead of the rows, and the + /// placeholder flow still covers rows streamed in during scrolling. + /// + [Parameter] public RenderFragment? LoadingTemplate { get; set; } + + /// + /// Optionally links this instance with a model, + /// causing the grid to fetch and render only the current page of data. + /// + /// This is normally used in conjunction with a component or some other UI logic + /// that displays and updates the supplied instance. + /// + [Parameter] public BitDataGridPaginationState? Pagination { get; set; } + + /// + /// If true, renders draggable handles around the column headers, allowing the user to resize the columns + /// manually. Size changes are not persisted. + /// + [Parameter] public bool ResizableColumns { get; set; } + + /// + /// The CSS class of all rows of the data grid. + /// + [Parameter] public string? RowClass { get; set; } + + /// + /// The function to generate the CSS class of each row of the data grid. + /// + [Parameter] public Func? RowClassSelector { get; set; } + + /// + /// The CSS style of all row of the data grid. + /// + [Parameter] public string? RowStyle { get; set; } + + /// + /// The function to generate the CSS style of each row of the data grid. + /// + [Parameter] public Func? RowStyleSelector { get; set; } + + /// + /// Optional template to customize row rendering. Receives with + /// set to the default row content; call it to render the original cells or replace with custom content. + /// + [Parameter] public RenderFragment>? RowTemplate { get; set; } + + /// + /// A theme name, with default value "default". This affects which styling rules match the table. + /// + [Parameter] public string? Theme { get; set; } = "default"; + + /// + /// If true, the grid will be rendered with virtualization. This is normally used in conjunction with + /// scrolling and causes the grid to fetch and render only the data around the current scroll viewport. + /// This can greatly improve the performance when scrolling through large data sets. + /// + /// If you use , you should supply a value for and must + /// ensure that every row renders with the same constant height. + /// + /// Generally it's preferable not to use if the amount of data being rendered + /// is small or if you are using pagination. + /// + [Parameter] public bool Virtualize { get; set; } + + + + /// + /// Sets the grid's current sort column to the specified . + /// + /// The column that defines the new sort order. + /// The direction of sorting. If the value is , then it will toggle the direction on each call. + /// A representing the completion of the operation. + public Task SortByColumnAsync(BitDataGridColumnBase column, BitDataGridSortDirection direction = BitDataGridSortDirection.Auto) + { + _sortByAscending = direction switch + { + BitDataGridSortDirection.Ascending => true, + BitDataGridSortDirection.Descending => false, + BitDataGridSortDirection.Auto => _sortByColumn == column ? !_sortByAscending : true, + _ => throw new NotSupportedException($"Unknown sort direction {direction}"), + }; + + _sortByColumn = column; + + StateHasChanged(); // We want to see the updated sort order in the header, even before the data query is completed + return RefreshDataAsync(); + } + + /// + /// Displays the UI for the specified column, closing any other column + /// options UI that was previously displayed. + /// + /// The column whose options are to be displayed, if any are available. + public void ShowColumnOptions(BitDataGridColumnBase column) + { + _displayOptionsForColumn = column; + _checkColumnOptionsPosition = true; // Triggers a call to JS to position the options element, apply autofocus, and any other setup + StateHasChanged(); + } + + /// + /// Instructs the grid to re-fetch and render the current data from the supplied data source + /// (either or ). + /// + /// A that represents the completion of the operation. + public async Task RefreshDataAsync() + { + try + { + await RefreshDataCoreAsync(); + } + finally + { + // Always rerender after the core refresh settles, even when it throws, so the grid + // doesn't get stuck showing the loading state if the caller handles the exception. + StateHasChanged(); + } + } + + + + // Invoked by descendant columns at a special time during rendering + internal void AddColumn(BitDataGridColumnBase column, BitDataGridSortDirection? isDefaultSortDirection) + { + if (_collectingColumns) + { + _columns.Add(column); + + if (_sortByColumn is null && isDefaultSortDirection.HasValue) + { + _sortByColumn = column; + _sortByAscending = isDefaultSortDirection.Value != BitDataGridSortDirection.Descending; + } + + // Remember the first default-sort column collected in this pass even when a (possibly stale) + // _sortByColumn is still set. If that prior column turns out to have been dropped, this lets + // FinishCollectingColumns switch to the newly declared default instead of clearing sorting. + if (isDefaultSortDirection.HasValue && _defaultSortColumnDuringCollect is null) + { + _defaultSortColumnDuringCollect = column; + _defaultSortDirectionDuringCollect = isDefaultSortDirection.Value; + } + } + } + + + + /// + protected override Task OnParametersSetAsync() + { + // The associated pagination state may have been added/removed/replaced + _currentPageItemsChanged.SubscribeOrMove(Pagination?.CurrentPageItemsChanged); + + if (Items is not null && ItemsProvider is not null) + { + throw new InvalidOperationException($"BitDataGrid requires one of {nameof(Items)} or {nameof(ItemsProvider)}, but both were specified."); + } + + // Perform a re-query only if the data source or something else has changed + var _newItemsOrItemsProvider = Items ?? (object?)ItemsProvider; + var dataSourceHasChanged = _newItemsOrItemsProvider != _lastAssignedItemsOrProvider; + if (dataSourceHasChanged) + { + _lastAssignedItemsOrProvider = _newItemsOrItemsProvider; + _asyncQueryExecutor = AsyncQueryExecutorSupplier.GetAsyncQueryExecutor(_services, Items); + } + + var mustRefreshData = dataSourceHasChanged + || (_lastRefreshedVirtualize != Virtualize) + || (ComputePaginationStateHash() != _lastRefreshedPaginationStateHash); + + // We don't want to trigger the first data load until we've collected the initial set of columns, + // because they might perform some action like setting the default sort order, so it would be wasteful + // to have to re-query immediately + if (_columns.Count > 0 && mustRefreshData) + { + return RefreshDataCoreAsync(); + } + + return Task.CompletedTask; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _jsEventDisposable = await _js.BitDataGridInit(_tableReference); + _lastInitColumnsHash = ComputeColumnsHash(); + _lastResizableColumns = ResizableColumns; + } + else if (ResizableColumns) + { + // The resize handles (.bit-qkg-drg) are bound per-element by init. When the column set + // changes, the header re-renders with fresh handles that have no listeners, so rebind them. + // The handles also appear/disappear when ResizableColumns itself is toggled, so rebind on + // that transition too. Re-running init re-adds the document-level listeners, so stop the + // previous registration first to avoid leaking duplicate handlers. Unchanged renders are skipped. + var hash = ComputeColumnsHash(); + if (hash != _lastInitColumnsHash || !_lastResizableColumns) + { + _lastInitColumnsHash = hash; + _lastResizableColumns = true; + await StopJsEventsAsync(); + _jsEventDisposable = await _js.BitDataGridInit(_tableReference); + } + } + else if (_lastResizableColumns) + { + // ResizableColumns was just turned off; the drag handles are gone. Rebind so the + // document-level listeners are refreshed and the stale handle registration is dropped. + _lastResizableColumns = false; + await StopJsEventsAsync(); + _jsEventDisposable = await _js.BitDataGridInit(_tableReference); + } + + if (_checkColumnOptionsPosition && _displayOptionsForColumn is not null) + { + _checkColumnOptionsPosition = false; + await _js.BitDataGridCheckColumnOptionsPosition(_tableReference); + } + + if (_queueSortReconciliationRefresh) + { + // Column recollection dropped the active sort column; re-query so the grid data matches + // the header state that no longer shows that sort. + _queueSortReconciliationRefresh = false; + await RefreshDataAsync(); + } + } + + private int ComputeColumnsHash() + { + var hash = new HashCode(); + foreach (var col in _columns) hash.Add(col); + return hash.ToHashCode(); + } + + // Only the requested slice inputs (page index + page size) should trigger a re-query. The pagination + // state's own GetHashCode also folds in TotalItemCount, which the grid mutates after every successful + // load, so using it here would make a completed fetch look like a fresh pagination change and kick off + // an immediate redundant second query. + private int? ComputePaginationStateHash() + => Pagination is null ? null : HashCode.Combine(Pagination.CurrentPageIndex, Pagination.ItemsPerPage); + + private async Task StopJsEventsAsync() + { + try + { + if (_jsEventDisposable is not null) + { + await _jsEventDisposable.InvokeVoidAsync("stop"); + await _jsEventDisposable.DisposeAsync(); + _jsEventDisposable = null; + } + } + catch (JSDisconnectedException) { } + catch (JSException) { } + } + + + + private void StartCollectingColumns() + { + _sortByColumnBeforeCollect = _sortByColumn; + _defaultSortColumnDuringCollect = null; + _defaultSortDirectionDuringCollect = null; + _columns.Clear(); + _collectingColumns = true; + } + + private void FinishCollectingColumns() + { + _collectingColumns = false; + + // The column that drove the last data load may no longer be among the freshly collected + // columns (it was removed or replaced). Leaving _sortByColumn pointing at a dropped column + // desyncs the data query from the header, so reconcile it and queue a refresh so the grid + // re-queries. The refresh is run from OnAfterRenderAsync because this runs mid-render. + if (_sortByColumn is not null && _columns.Contains(_sortByColumn) is false) + { + if (_defaultSortColumnDuringCollect is not null) + { + // A newly collected column declares a default sort, so adopt it instead of clearing + // sorting; otherwise a dynamic column swap would drop the intended default order. + _sortByColumn = _defaultSortColumnDuringCollect; + _sortByAscending = _defaultSortDirectionDuringCollect!.Value != BitDataGridSortDirection.Descending; + } + else + { + _sortByColumn = null; + _sortByAscending = false; + } + _queueSortReconciliationRefresh = true; + } + else if (_columnsCollectedOnce && _sortByColumnBeforeCollect is null && _sortByColumn is not null) + { + // A recollection assigned a brand-new default sort (none was active before). The initial + // collection already loads data via ColumnsFirstCollected, but later recollections do not, + // so queue a refresh to re-query in the newly defaulted sort order and keep header/data in sync. + _queueSortReconciliationRefresh = true; + } + + _columnsCollectedOnce = true; + } + + // Same as RefreshDataAsync, except without forcing a re-render. We use this from OnParametersSetAsync + // because in that case there's going to be a re-render anyway. + private async Task RefreshDataCoreAsync() + { + // Record the Virtualize mode this refresh runs under so every refresh path keeps the marker + // current: the initial column-driven load (via ColumnsFirstCollected), RefreshDataAsync, and the + // parameter-change trigger in OnParametersSetAsync all funnel through here. Updating it only in + // OnParametersSetAsync would leave it stale after those other paths, so a later parameter set + // could wrongly (or never) detect a virtualized/non-virtualized flip. + _lastRefreshedVirtualize = Virtualize; + + // Snapshot the requested pagination slice up front so both the virtualized and non-virtualized + // paths record it. Doing this only in the non-virtualized branch (and relying on + // ProvideVirtualizedItems otherwise) leaves the marker stale when Virtualize is on but its child + // hasn't requested items yet (e.g. first render / right after toggling virtualization). The next + // OnParametersSetAsync would then see ComputePaginationStateHash() != _lastRefreshedPaginationStateHash + // and fire a duplicate initial query for the same slice. + _lastRefreshedPaginationStateHash = ComputePaginationStateHash(); + + // Move into a "loading" state, cancelling any earlier-but-still-pending load. Do NOT dispose + // the previous source here: the load that owns it may still be in flight and holding its token + // (e.g. registered on it), so disposing now could surface an ObjectDisposedException instead of + // the expected OperationCanceledException. Each load disposes its own source in its finally block + // once it has finished using it (whether or not it is still the current one), so a superseded + // source is disposed by its owning load rather than leaked to the GC. + _pendingDataLoadCancellationTokenSource?.Cancel(); + var thisLoadCts = _pendingDataLoadCancellationTokenSource = new CancellationTokenSource(); + + // Render now so the loading state (IsLoading / LoadingTemplate) becomes visible as soon as the + // refresh starts, instead of only after the async load below completes. + StateHasChanged(); + + if (Virtualize) + { + // If we're using Virtualize, we have to go through its RefreshDataAsync API otherwise: + // (1) It won't know to update its own internal state if the provider output has changed + // (2) We won't know what slice of data to query for + // The reference can still be null before it's captured (first render) or right after toggling + // virtualization on; in that case Virtualize will request its own items once it renders, so we + // just reconcile the load-state here. The non-virtualized provider request must never run for a + // virtualized grid. + try + { + if (_virtualizeComponent is not null) + { + await _virtualizeComponent.RefreshDataAsync(); + } + } + finally + { + // Always reconcile the load-state, even if RefreshDataAsync threw, so we don't leak the + // CTS or leave _pendingDataLoadCancellationTokenSource pointing at a disposed instance. + // This load is done with its own source, so dispose it unconditionally; only clear the + // field when it still points at this source (a newer load may already own it). + thisLoadCts.Dispose(); + if (ReferenceEquals(_pendingDataLoadCancellationTokenSource, thisLoadCts)) + { + _pendingDataLoadCancellationTokenSource = null; + } + } + } + else + { + // If we're not using Virtualize, we build and execute a request against the items provider directly + var startIndex = Pagination is null ? 0 : (Pagination.CurrentPageIndex * Pagination.ItemsPerPage); + var request = new BitDataGridItemsProviderRequest( + startIndex, Pagination?.ItemsPerPage, _sortByColumn, _sortByAscending, thisLoadCts.Token); + try + { + var result = await ResolveItemsRequestAsync(request); + if (!thisLoadCts.IsCancellationRequested) + { + _currentNonVirtualizedViewItems = result.Items; + _ariaBodyRowCount = _currentNonVirtualizedViewItems.Count; + await (Pagination?.SetTotalItemCountAsync(result.TotalItemCount) ?? Task.CompletedTask); + } + } + catch (OperationCanceledException) when (thisLoadCts.IsCancellationRequested) + { + // This load was superseded by a newer request (our own cancellation token fired); swallow + // the cancellation and fall through to the cleanup below so the load-state remains + // consistent. Cancellations from any other source (e.g. a provider-side timeout) propagate. + } + finally + { + // This load is done with its own source, so dispose it unconditionally to avoid leaking + // a superseded source; only clear the field when it still points at this source. + thisLoadCts.Dispose(); + if (ReferenceEquals(_pendingDataLoadCancellationTokenSource, thisLoadCts)) + { + _pendingDataLoadCancellationTokenSource = null; + } + } + } + } + + // Gets called both by RefreshDataCoreAsync and directly by the Virtualize child component during scrolling + private async ValueTask> ProvideVirtualizedItems(ItemsProviderRequest request) + { + _lastRefreshedPaginationStateHash = ComputePaginationStateHash(); + + // Debounce the requests. This eliminates a lot of redundant queries at the cost of slight lag after interactions. + // TODO: Consider making this configurable, or smarter (e.g., doesn't delay on first call in a batch, then the amount + // of delay increases if you rapidly issue repeated requests, such as when scrolling a long way) + try + { + await Task.Delay(100, request.CancellationToken); + } + catch (OperationCanceledException) + { + // The request was superseded/cancelled during the debounce window; abandon it early. + return default; + } + if (request.CancellationToken.IsCancellationRequested) + { + return default; + } + + // Combine the query parameters from Virtualize with the ones from PaginationState + var startIndex = request.StartIndex; + var count = request.Count; + if (Pagination is not null) + { + startIndex += Pagination.CurrentPageIndex * Pagination.ItemsPerPage; + count = Math.Max(0, Math.Min(request.Count, Pagination.ItemsPerPage - request.StartIndex)); + } + + var providerRequest = new BitDataGridItemsProviderRequest( + startIndex, count, _sortByColumn, _sortByAscending, request.CancellationToken); + BitDataGridItemsProviderResult providerResult; + try + { + providerResult = await ResolveItemsRequestAsync(providerRequest); + } + catch (OperationCanceledException) when (request.CancellationToken.IsCancellationRequested) + { + // The request was superseded by a newer one after the debounce window (our own cancellation + // token fired); the items provider observed the cancellation token and bailed out. Return an + // empty result the virtualization system can handle rather than letting the cancellation + // propagate out of here. Cancellations from any other source propagate as real errors. + return default; + } + + if (!request.CancellationToken.IsCancellationRequested) + { + // ARIA's rowcount is part of the UI, so it should reflect what the human user regards as the number of rows in the table, + // not the number of physical elements. For virtualization this means what's in the entire scrollable range, not just + // the current viewport. In the case where you're also paginating then it means what's conceptually on the current page. + // The last page can hold fewer than ItemsPerPage rows, so clamp the paginated count to the items remaining on the current + // page; otherwise assistive tech would announce non-existent trailing rows on a short final page. + _ariaBodyRowCount = Pagination is null + ? providerResult.TotalItemCount + : Math.Clamp(providerResult.TotalItemCount - Pagination.CurrentPageIndex * Pagination.ItemsPerPage, 0, Pagination.ItemsPerPage); + + await (Pagination?.SetTotalItemCountAsync(providerResult.TotalItemCount) ?? Task.CompletedTask); + + // We're supplying the row index along with each row's data because we need it for aria-rowindex, and we have to account for + // the virtualized start index. It might be more performant just to have some _latestQueryRowStartIndex field, but we'd have + // to make sure it doesn't get out of sync with the rows being rendered. + return new ItemsProviderResult<(int, TGridItem)>( + items: providerResult.Items.Select((x, i) => ValueTuple.Create(i + request.StartIndex + 2, x)), + totalItemCount: _ariaBodyRowCount); + } + + return default; + } + + // Normalizes all the different ways of configuring a data source so they have common GridItemsProvider-shaped API + private async ValueTask> ResolveItemsRequestAsync(BitDataGridItemsProviderRequest request) + { + if (ItemsProvider is not null) + { + return await ItemsProvider(request); + } + else if (Items is not null) + { + var totalItemCount = _asyncQueryExecutor is null ? Items.Count() : await _asyncQueryExecutor.CountAsync(Items); + var result = request.ApplySorting(Items).Skip(request.StartIndex); + if (request.Count.HasValue) + { + result = result.Take(request.Count.Value); + } + var resultArray = _asyncQueryExecutor is null ? result.ToArray() : await _asyncQueryExecutor.ToArrayAsync(result); + return BitDataGridItemsProviderResult.From(resultArray, totalItemCount); + } + else + { + return BitDataGridItemsProviderResult.From(Array.Empty(), 0); + } + } + + private string AriaSortValue(BitDataGridColumnBase column) + => _sortByColumn == column + ? (_sortByAscending ? "ascending" : "descending") + : "none"; + + private string? ColumnHeaderClass(BitDataGridColumnBase column) + => _sortByColumn == column + ? $"{ColumnClass(column)} {(_sortByAscending ? "bit-qkg-csa" : "bit-qkg-csd")}" + : ColumnClass(column); + + private string GridClass() + => $"bit-qkg {Class} {((IsLoading && LoadingTemplate is null) ? "loading" : null)}".Trim(); + + private void CloseColumnOptions() + { + _displayOptionsForColumn = null; + } + + private string? GetRowClass(TGridItem item) + { + var selected = RowClassSelector?.Invoke(item); + + if (string.IsNullOrEmpty(RowClass)) return string.IsNullOrEmpty(selected) ? null : selected; + if (string.IsNullOrEmpty(selected)) return RowClass; + return $"{RowClass} {selected}"; + } + + private string? GetRowStyle(TGridItem item) + { + var selected = RowStyleSelector?.Invoke(item); + + if (string.IsNullOrEmpty(RowStyle)) return string.IsNullOrEmpty(selected) ? null : selected; + if (string.IsNullOrEmpty(selected)) return RowStyle; + return $"{RowStyle};{selected}"; + } + + + + private static string? ColumnClass(BitDataGridColumnBase column) => column.Align switch + { + BitDataGridAlign.Center => $"bit-qkg-cjc {column.Class}", + BitDataGridAlign.Right => $"bit-qkg-cje {column.Class}", + _ => column.Class, + }; + + + + + /// + public async ValueTask DisposeAsync() + { + await DisposeAsync(true); + GC.SuppressFinalize(this); + } + + protected virtual async ValueTask DisposeAsync(bool disposing) + { + if (_disposed || disposing is false) return; + + // Cancel (but don't dispose) any in-flight load: the load that owns this source may still be + // holding its token, so disposing here could race into an ObjectDisposedException. The owning + // load disposes it in its finally block (it's still the current source during disposal), so we + // only signal cancellation here. + _pendingDataLoadCancellationTokenSource?.Cancel(); + + _currentPageItemsChanged.Dispose(); + + try + { + if (_jsEventDisposable is not null) + { + await _jsEventDisposable.InvokeVoidAsync("stop"); + await _jsEventDisposable.DisposeAsync(); + } + + //if (_jsModule is not null) + //{ + // await _jsModule.DisposeAsync(); + //} + } + catch (JSDisconnectedException) + { + // The JS side may routinely be gone already if the reason we're disposing is that + // the client disconnected. This is not an error. + } + catch (JSException ex) + { + // it seems it's safe to just ignore this exception here. + // otherwise it will blow up the MAUI app in a page refresh for example. + Console.WriteLine(ex.Message); + } + + _disposed = true; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGrid.scss b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGrid.scss new file mode 100644 index 0000000000..1109021ff7 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGrid.scss @@ -0,0 +1,138 @@ +.bit-qkg { + width: 100%; + --bit-qkg-col-gap: 1rem; + + th { + position: relative; + } + + > thead > tr > th { + font-weight: normal; + } + + &.loading > tbody { + opacity: 0.25; + transition: opacity linear 100ms 25ms; + } + + > tbody > tr > td { + padding: 0.1rem calc(0.4rem + var(--bit-qkg-col-gap)) 0.1rem 0.4rem; + } +} + + +.bit-qkg-hct { + display: flex; + position: relative; + align-items: center; + padding-inline-end: var(--bit-qkg-col-gap); +} + +.bit-qkg-cop { + z-index: 1; + padding: 1rem; + border: 1px solid; + position: absolute; + inset-inline-start: 0; + border-color: var(--bit-clr-brd-pri); + background-color: var(--bit-clr-bg-sec); +} + +.bit-qkg-cob { + width: 1.5rem; + background: unset; + + &::before { + content: "\E712"; + font-style: normal; + font-weight: normal; + display: inline-block; + font-family: 'Fabric MDL2 bit BlazorUI Extras'; + } +} + +.bit-qkg-drg { + width: 1rem; + cursor: ew-resize; + position: absolute; + inset-block-end: 0; + inset-block-start: 0; + inset-inline-end: calc(var(--bit-qkg-col-gap)/2 - 0.5rem); + + &::after { + content: ' '; + position: absolute; + inset-block-end: 5px; + inset-block-start: 5px; + inset-inline-start: 0.5rem; + border-color: var(--bit-clr-brd-pri); + border-inline-start: 1px solid var(--bit-clr-brd-pri); + } +} + +.bit-qkg-srt { + width: 1rem; + height: 1rem; + opacity: 0.5; + align-self: center; + text-align: center; +} + +.bit-qkg-csa .bit-qkg-srt::before, +.bit-qkg-csd .bit-qkg-srt::before { + content: "\E96F"; + font-style: normal; + font-weight: normal; + display: inline-block; + transform: rotate(90deg); + font-family: 'Fabric MDL2 bit BlazorUI Extras'; +} + +.bit-qkg-csd .bit-qkg-srt { + transform: scaleY(-1) translateY(-2px); +} + +.bit-qkg-ctl { + gap: 0.4rem; + flex-grow: 1; + display: flex; + min-width: 0px; + font-size: 1rem; + font-weight: bold; + padding: 0.1rem 0.4rem; +} + +button.bit-qkg-ctl { + border: none; + color: inherit; + cursor: pointer; + background: none; + position: relative; +} + +.bit-qkg-ctt { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.bit-qkg-cjc { + text-align: center; + + .bit-qkg-ctl { + justify-content: center; + } +} + +.bit-qkg-cje { + text-align: end; + + .bit-qkg-ctl { + flex-direction: row-reverse; + } +} + +.bit-qkg-plh::after { + opacity: 0.75; + content: '\2026'; +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGrid.ts b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGrid.ts new file mode 100644 index 0000000000..b579bc967b --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGrid.ts @@ -0,0 +1,151 @@ +namespace BitBlazorUI.Legacy { + export class DataGrid { + public static init(tableElement: any) { + // Tracks the drag handles this init() bound so stop() can remove their listeners too, + // preventing handlers from accumulating across repeated init()/stop() cycles. + const boundDragHandles: { handle: any, listener: any }[] = []; + // Holds the teardown for an in-progress column resize drag (the document-level move/up + // listeners installed by handleMouseDown) so stop() can detach them even if disposal + // happens mid-drag, before the pointer is released. + const dragState: { cleanup: (() => void) | null } = { cleanup: null }; + DataGrid.enableColumnResizing(tableElement, boundDragHandles, dragState); + + const bodyClickHandler = (event: any) => { + const columnOptionsElement = tableElement.tHead.querySelector('.bit-qkg-cop'); + if (columnOptionsElement && event.composedPath().indexOf(columnOptionsElement) < 0) { + tableElement.dispatchEvent(new CustomEvent('closecolumnoptions', { bubbles: true })); + } + }; + const keyDownHandler = (event: any) => { + const columnOptionsElement = tableElement.tHead.querySelector('.bit-qkg-cop'); + if (columnOptionsElement && event.key === "Escape") { + tableElement.dispatchEvent(new CustomEvent('closecolumnoptions', { bubbles: true })); + } + }; + + document.body.addEventListener('click', bodyClickHandler); + document.body.addEventListener('mousedown', bodyClickHandler); // Otherwise it seems strange that it doesn't go away until you release the mouse button + document.body.addEventListener('keydown', keyDownHandler); + + return { + stop: () => { + document.body.removeEventListener('click', bodyClickHandler); + document.body.removeEventListener('mousedown', bodyClickHandler); + document.body.removeEventListener('keydown', keyDownHandler); + + // Detach any document-level listeners left by an in-progress resize drag and clear + // the active drag state, so disposal/re-init mid-drag can't keep mutating a stale th. + if (dragState.cleanup) { + dragState.cleanup(); + dragState.cleanup = null; + } + + // Remove the per-handle drag listeners and clear the bound marker so a later + // init() can rebind the same surviving elements without duplicating handlers. + boundDragHandles.forEach(({ handle, listener }) => { + handle.removeEventListener('mousedown', listener); + handle.removeEventListener('touchstart', listener); + delete handle.__bitQkgResizeBound; + }); + boundDragHandles.length = 0; + } + }; + } + + public static checkColumnOptionsPosition(tableElement: any) { + const colOptions = tableElement.tHead && tableElement.tHead.querySelector('.bit-qkg-cop'); // Only match within *our* thead, not nested tables + if (colOptions) { + // We want the options popup to be positioned over the grid, not overflowing on either side, because it's possible that + // beyond either side is off-screen or outside the scroll range of an ancestor + const gridRect = tableElement.getBoundingClientRect(); + const optionsRect = colOptions.getBoundingClientRect(); + const leftOverhang = Math.max(0, gridRect.left - optionsRect.left); + const rightOverhang = Math.max(0, optionsRect.right - gridRect.right); + if (leftOverhang || rightOverhang) { + // In the unlikely event that it overhangs both sides, we'll center it + const applyOffset = leftOverhang && rightOverhang ? (leftOverhang - rightOverhang) / 2 : (leftOverhang - rightOverhang); + colOptions.style.transform = `translateX(${applyOffset}px)`; + } else { + // Clear any offset left over from a previous opening so the popup isn't misaligned. + colOptions.style.transform = ''; + } + + if (typeof colOptions.scrollIntoViewIfNeeded === 'function') { + colOptions.scrollIntoViewIfNeeded(); + } else { + // Fall back to a nearest-edge scroll so browsers without scrollIntoViewIfNeeded + // don't scroll more aggressively than needed (the default scrollIntoView() can + // jump the popup fully into view and shift the grid). + colOptions.scrollIntoView({ block: 'nearest', inline: 'nearest' }); + } + + const autoFocusElem = colOptions.querySelector('[autofocus]'); + if (autoFocusElem) { + autoFocusElem.focus(); + } + } + } + + private static enableColumnResizing(tableElement: any, boundDragHandles: { handle: any, listener: any }[], dragState: { cleanup: (() => void) | null }) { + tableElement.tHead.querySelectorAll('.bit-qkg-drg').forEach((handle: any) => { + // Bind each handle only once. A surviving handle (reused by Blazor's diffing across + // re-renders) would otherwise accumulate a fresh listener on every init() call. + if (handle.__bitQkgResizeBound) return; + handle.__bitQkgResizeBound = true; + + handle.addEventListener('mousedown', handleMouseDown); + if ('ontouchstart' in window) { + handle.addEventListener('touchstart', handleMouseDown); + } + boundDragHandles.push({ handle, listener: handleMouseDown }); + + function handleMouseDown(evt: any) { + evt.preventDefault(); + evt.stopPropagation(); + + const th = handle.parentElement; + const startPageX = evt.touches ? evt.touches[0].pageX : evt.pageX; + const originalColumnWidth = th.offsetWidth; + const rtlMultiplier = window.getComputedStyle(th, null).getPropertyValue('direction') === 'rtl' ? -1 : 1; + let updatedColumnWidth = 0; + + function handleMouseMove(evt: any) { + evt.stopPropagation(); + const newPageX = evt.touches ? evt.touches[0].pageX : evt.pageX; + // Clamp to a minimum width so a column can't collapse to (or below) zero while dragging. + const minColumnWidth = 20; + const nextWidth = Math.max(minColumnWidth, originalColumnWidth + (newPageX - startPageX) * rtlMultiplier); + if (Math.abs(nextWidth - updatedColumnWidth) > 0) { + updatedColumnWidth = nextWidth; + th.style.width = `${updatedColumnWidth}px`; + } + } + + function handleMouseUp() { + document.body.removeEventListener('mousemove', handleMouseMove); + document.body.removeEventListener('mouseup', handleMouseUp); + document.body.removeEventListener('touchmove', handleMouseMove); + document.body.removeEventListener('touchend', handleMouseUp); + document.body.removeEventListener('touchcancel', handleMouseUp); + dragState.cleanup = null; + } + + if (window.TouchEvent && evt instanceof TouchEvent) { + document.body.addEventListener('touchmove', handleMouseMove, { passive: true }); + document.body.addEventListener('touchend', handleMouseUp, { passive: true }); + // A touch gesture can be interrupted (e.g. by the system) without firing touchend, + // which would leave the move/end listeners attached. Tear down on touchcancel too. + document.body.addEventListener('touchcancel', handleMouseUp, { passive: true }); + } else { + document.body.addEventListener('mousemove', handleMouseMove, { passive: true }); + document.body.addEventListener('mouseup', handleMouseUp, { passive: true }); + } + + // Expose this drag's teardown so stop() can detach the document-level listeners if + // the grid is disposed/re-initialized before the pointer is released. + dragState.cleanup = handleMouseUp; + } + }); + } + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGridJsRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGridJsRuntimeExtensions.cs new file mode 100644 index 0000000000..0a1bd96c55 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGridJsRuntimeExtensions.cs @@ -0,0 +1,14 @@ +namespace Bit.BlazorUI.Legacy; + +internal static class BitDataGridJsRuntimeExtensions +{ + public static async ValueTask BitDataGridInit(this IJSRuntime jsRuntime, ElementReference tableElement) + { + return await jsRuntime.InvokeAsync("BitBlazorUI.Legacy.DataGrid.init", tableElement); + } + + public static async ValueTask BitDataGridCheckColumnOptionsPosition(this IJSRuntime jsRuntime, ElementReference tableElement) + { + await jsRuntime.InvokeVoidAsync("BitBlazorUI.Legacy.DataGrid.checkColumnOptionsPosition", tableElement); + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGridRowTemplateArgs.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGridRowTemplateArgs.cs new file mode 100644 index 0000000000..6c2b2702f2 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGridRowTemplateArgs.cs @@ -0,0 +1,24 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Arguments passed to the render fragment. +/// +/// The type of data represented by each row in the grid. +public class BitDataGridRowTemplateArgs +{ + /// + /// A render fragment that produces the original row markup (the default <tr> with all column cells). + /// Render this in your custom template to include the default row, or omit it to replace entirely. + /// + public required RenderFragment OriginalRow { get; set; } + + /// + /// The 1-based row index used for accessibility (e.g. aria-rowindex). + /// + public int RowIndex { get; set; } + + /// + /// The data item for this row. + /// + public T RowItem { get; set; } = default!; +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGridSortDirection.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGridSortDirection.cs new file mode 100644 index 0000000000..175b45aab7 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGridSortDirection.cs @@ -0,0 +1,24 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Describes the direction in which a column is sorted. +/// +public enum BitDataGridSortDirection +{ + /// + /// Ascending order. + /// + Ascending, + + /// + /// Descending order. + /// + Descending, + + /// + /// Automatic sort order. When used with , + /// the sort order will automatically toggle between and on successive calls, and + /// resets to whenever the specified column is changed. + /// + Auto, +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridAlign.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridAlign.cs new file mode 100644 index 0000000000..6bb473cf67 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridAlign.cs @@ -0,0 +1,22 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Describes alignment for a column. +/// +public enum BitDataGridAlign +{ + /// + /// Justifies the content against the start of the container. + /// + Left, + + /// + /// Justifies the content at the center of the container. + /// + Center, + + /// + /// Justifies the content at the end of the container. + /// + Right, +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridColumnBase.razor b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridColumnBase.razor new file mode 100644 index 0000000000..75d9a39c5c --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridColumnBase.razor @@ -0,0 +1,148 @@ +@namespace Bit.BlazorUI.Legacy +@typeparam TGridItem + +@{ + InternalGridContext.Grid.AddColumn(this, IsDefaultSort); +} + +@code +{ + [CascadingParameter] internal InternalGridContext InternalGridContext { get; set; } = default!; + + /// + /// Title text for the column. This is rendered automatically if is not used. + /// + [Parameter] public string? Title { get; set; } + + /// + /// An optional CSS class name. If specified, this is included in the class attribute of table header and body cells + /// for this column. + /// + [Parameter] public string? Class { get; set; } + + /// + /// If specified, controls the justification of table header and body cells for this column. + /// + [Parameter] public BitDataGridAlign Align { get; set; } + + /// + /// An optional template for this column's header cell. If not specified, the default header template + /// includes the along with any applicable sort indicators and options buttons. + /// + [Parameter] public RenderFragment>? HeaderTemplate { get; set; } + + /// + /// If specified, indicates that this column has this associated options UI. A button to display this + /// UI will be included in the header cell by default. + /// + /// If is used, it is left up to that template to render any relevant + /// "show options" UI and invoke the grid's ). + /// + [Parameter] public RenderFragment? ColumnOptions { get; set; } + + /// + /// Indicates whether the data should be sortable by this column. + /// + /// The default value may vary according to the column type (for example, a + /// is sortable by default if any parameter is specified). + /// + [Parameter] public bool? Sortable { get; set; } + + /// + /// If specified and not null, indicates that this column represents the initial sort order + /// for the grid. The supplied value controls the default sort direction. + /// + [Parameter] public BitDataGridSortDirection? IsDefaultSort { get; set; } + + /// + /// If specified, virtualized grids will use this template to render cells whose data has not yet been loaded. + /// + [Parameter] public RenderFragment? PlaceholderTemplate { get; set; } + + /// + /// Gets a reference to the enclosing . + /// + public BitDataGrid Grid => InternalGridContext.Grid; + + /// + /// Overridden by derived components to provide rendering logic for the column's cells. + /// + /// The current . + /// The data for the row being rendered. + protected internal abstract void CellContent(RenderTreeBuilder builder, TGridItem item); + + /// + /// Gets or sets a that will be rendered for this column's header cell. + /// This allows derived components to change the header output. However, derived components are then + /// responsible for using within that new output if they want to continue + /// respecting that option. + /// + protected internal RenderFragment HeaderContent { get; protected set; } + + /// + /// Get a value indicating whether this column should act as sortable if no value was set for the + /// parameter. The default behavior is not to be + /// sortable unless is true. + /// + /// Derived components may override this to implement alternative default sortability rules. + /// + /// True if the column should be sortable by default, otherwise false. + protected virtual bool IsSortableByDefault() => false; + + /// + /// Constructs an instance of . + /// + public BitDataGridColumnBase() + { + HeaderContent = RenderDefaultHeaderContent; + } + + private string SortButtonLabel() + => string.IsNullOrEmpty(Title) ? "Sort" : $"Sort by {Title}"; + + private string ColumnOptionsLabel() + => string.IsNullOrEmpty(Title) ? "Column options" : $"Column options for {Title}"; + + private void RenderDefaultHeaderContent(RenderTreeBuilder __builder) + { + @if (HeaderTemplate is not null) + { + @HeaderTemplate(this) + } + else + { + @if (ColumnOptions is not null && Align != BitDataGridAlign.Right) + { + + } + + if (Sortable.HasValue ? Sortable.Value : IsSortableByDefault()) + { + + } + else + { +
+
@Title
+
+ } + + @if (ColumnOptions is not null && Align == BitDataGridAlign.Right) + { + + } + } + } + + internal void RenderPlaceholderContent(RenderTreeBuilder __builder, PlaceholderContext placeholderContext) + { + // Blank if no placeholder template was supplied, as it's enough to style with CSS by default + if (PlaceholderTemplate is not null) + { + @PlaceholderTemplate(placeholderContext) + } + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridColumnBase.razor.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridColumnBase.razor.cs new file mode 100644 index 0000000000..0e1915a04c --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridColumnBase.razor.cs @@ -0,0 +1,9 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// An abstract base class for columns in a . +/// +/// The type of data represented by each row in the grid. +public abstract partial class BitDataGridColumnBase +{ +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridPropertyColumn.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridPropertyColumn.cs new file mode 100644 index 0000000000..7b735363dc --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridPropertyColumn.cs @@ -0,0 +1,116 @@ +using System.Linq.Expressions; + +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a column whose cells display a single value. +/// +/// The type of data represented by each row in the grid. +/// The type of the value being displayed in the column's cells. +public class BitDataGridPropertyColumn : BitDataGridColumnBase, IBitDataGridSortBuilderColumn +{ + private Expression>? _lastAssignedProperty; + private string? _lastAssignedFormat; + private bool _titleWasExplicitlySet; + private Func? _cellTextFunc; + private BitDataGridSort? _sortBuilder; + + /// + /// Defines the value to be displayed in this column's cells. + /// + [Parameter, EditorRequired] public Expression> Property { get; set; } = default!; + + /// + /// Optionally specifies a format string for the value. + /// + /// Using this requires the type to implement . + /// + [Parameter] public string? Format { get; set; } + + BitDataGridSort? IBitDataGridSortBuilderColumn.SortBuilder => _sortBuilder; + + + /// + public override Task SetParametersAsync(ParameterView parameters) + { + // Track whether Title was supplied explicitly by the consumer in *this* render's ParameterView + // rather than inferring intent from value equality (which can't tell an auto-derived header apart + // from an explicit one matching the member name). Recompute it every render instead of latching: + // if a consumer later removes Title, explicitness must drop back to false so the auto-generated + // header can return. + _titleWasExplicitlySet = parameters.TryGetValue(nameof(Title), out _); + + // When the consumer stops passing Title, the incoming ParameterView no longer contains it, so + // base.SetParametersAsync leaves the previously assigned (explicit) value in place. Clear it up + // front so a removed Title can't linger as a stale header; OnParametersSet then re-derives the + // auto-generated title from the Property when possible. + if (!_titleWasExplicitlySet) + { + Title = null; + } + + return base.SetParametersAsync(parameters); + } + + + /// + protected override void OnParametersSet() + { + // We have to do a bit of pre-processing on the lambda expression. Only do that if the Property + // or the Format has changed, so a Format-only change still rebuilds the cell formatter. + if (_lastAssignedProperty != Property || _lastAssignedFormat != Format) + { + var compiledPropertyExpression = Property.Compile(); + Func cellTextFunc; + + if (Format.HasValue()) + { + // For a nullable value type (e.g. int?, DateTime?) Nullable itself does not implement + // IFormattable, but its underlying type does and a boxed non-null value formats correctly. + // Check the underlying type so Format is allowed on nullable columns too. + var formattableType = Nullable.GetUnderlyingType(typeof(TProp)) ?? typeof(TProp); + if (typeof(IFormattable).IsAssignableFrom(formattableType)) + { + cellTextFunc = item => ((IFormattable?)compiledPropertyExpression!(item))?.ToString(Format, null); + } + else + { + throw new InvalidOperationException($"A '{nameof(Format)}' parameter was supplied, but the type '{typeof(TProp)}' does not implement '{typeof(IFormattable)}'."); + } + } + else + { + cellTextFunc = item => compiledPropertyExpression!(item)?.ToString(); + } + + _cellTextFunc = cellTextFunc; + _sortBuilder = BitDataGridSort.ByAscending(Property); + + // Only record the assignments after the formatter has been built and validated, so a failed + // Format/TProp validation above doesn't suppress a retry on the next parameters set (which + // would leave _cellTextFunc in a stale or null state). + _lastAssignedProperty = Property; + _lastAssignedFormat = Format; + } + + if (_titleWasExplicitlySet) + { + // The consumer supplied Title this render; base.SetParametersAsync already applied it, so + // there is nothing more to do. + } + else if (Property.Body is MemberExpression memberExpression) + { + // No explicit Title this render, so derive the header from the member name. Recomputed every + // render, this also lets the auto-generated header follow Property changes and reappear after + // a previously explicit Title is removed. + Title = memberExpression.Member.Name; + } + // else: Property is a method/cast expression (no member name to derive a header from) and no + // explicit Title was supplied. SetParametersAsync already cleared any stale value, so the column + // simply has no header. + } + + /// + protected internal override void CellContent(RenderTreeBuilder builder, TGridItem item) + => builder.AddContent(0, _cellTextFunc!(item)); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridSort.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridSort.cs new file mode 100644 index 0000000000..4c80157a0e --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridSort.cs @@ -0,0 +1,180 @@ +using System.Linq.Expressions; + +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a sort order specification used within . +/// +/// The type of data represented by each row in the grid. +public class BitDataGridSort +{ + private const string ExpressionNotRepresentableMessage = "The supplied expression can't be represented as a property name for sorting. Only simple member expressions, such as @(x => x.SomeProperty), can be converted to property names."; + + private Func, bool, IOrderedQueryable> _first; + private List, bool, IOrderedQueryable>>? _then; + + private (LambdaExpression, bool) _firstExpression; + private List<(LambdaExpression, bool)>? _thenExpressions; + + private IReadOnlyCollection<(string PropertyName, BitDataGridSortDirection Direction)>? _cachedPropertyListAscending; + private IReadOnlyCollection<(string PropertyName, BitDataGridSortDirection Direction)>? _cachedPropertyListDescending; + + internal BitDataGridSort(Func, bool, IOrderedQueryable> first, (LambdaExpression, bool) firstExpression) + { + _first = first; + _firstExpression = firstExpression; + _then = default; + _thenExpressions = default; + } + + /// + /// Produces a instance that sorts according to the specified , ascending. + /// + /// The type of the expression's value. + /// An expression defining how a set of instances are to be sorted. + /// A instance representing the specified sorting rule. + public static BitDataGridSort ByAscending(Expression> expression) + => new BitDataGridSort((queryable, asc) => asc ? queryable.OrderBy(expression) : queryable.OrderByDescending(expression), + (expression, true)); + + /// + /// Produces a instance that sorts according to the specified , descending. + /// + /// The type of the expression's value. + /// An expression defining how a set of instances are to be sorted. + /// A instance representing the specified sorting rule. + public static BitDataGridSort ByDescending(Expression> expression) + => new BitDataGridSort((queryable, asc) => asc ? queryable.OrderByDescending(expression) : queryable.OrderBy(expression), + (expression, false)); + + /// + /// Updates a instance by appending a further sorting rule. + /// + /// The type of the expression's value. + /// An expression defining how a set of instances are to be sorted. + /// A instance representing the specified sorting rule. + public BitDataGridSort ThenAscending(Expression> expression) + { + _then ??= new(); + _thenExpressions ??= new(); + _then.Add((queryable, asc) => asc ? queryable.ThenBy(expression) : queryable.ThenByDescending(expression)); + _thenExpressions.Add((expression, true)); + _cachedPropertyListAscending = null; + _cachedPropertyListDescending = null; + return this; + } + + /// + /// Updates a instance by appending a further sorting rule. + /// + /// The type of the expression's value. + /// An expression defining how a set of instances are to be sorted. + /// A instance representing the specified sorting rule. + public BitDataGridSort ThenDescending(Expression> expression) + { + _then ??= new(); + _thenExpressions ??= new(); + _then.Add((queryable, asc) => asc ? queryable.ThenByDescending(expression) : queryable.ThenBy(expression)); + _thenExpressions.Add((expression, false)); + _cachedPropertyListAscending = null; + _cachedPropertyListDescending = null; + return this; + } + + internal IOrderedQueryable Apply(IQueryable queryable, bool ascending) + { + var orderedQueryable = _first(queryable, ascending); + + if (_then is not null) + { + foreach (var clause in _then) + { + orderedQueryable = clause(orderedQueryable, ascending); + } + } + + return orderedQueryable; + } + + internal IReadOnlyCollection<(string PropertyName, BitDataGridSortDirection Direction)> ToPropertyList(bool ascending) + { + if (ascending) + { + _cachedPropertyListAscending ??= BuildPropertyList(ascending: true); + return _cachedPropertyListAscending; + } + else + { + _cachedPropertyListDescending ??= BuildPropertyList(ascending: false); + return _cachedPropertyListDescending; + } + } + + private IReadOnlyCollection<(string PropertyName, BitDataGridSortDirection Direction)> BuildPropertyList(bool ascending) + { + var result = new List<(string, BitDataGridSortDirection)>(); + result.Add((ToPropertyName(_firstExpression.Item1), (_firstExpression.Item2 ^ ascending) ? BitDataGridSortDirection.Descending : BitDataGridSortDirection.Ascending)); + + if (_thenExpressions is not null) + { + foreach (var (thenLambda, thenAscending) in _thenExpressions) + { + result.Add((ToPropertyName(thenLambda), (thenAscending ^ ascending) ? BitDataGridSortDirection.Descending : BitDataGridSortDirection.Ascending)); + } + } + + return result; + } + + // Not sure we really want this level of complexity, but it converts expressions like @(c => c.Medals.Gold) to "Medals.Gold" + private static string ToPropertyName(LambdaExpression expression) + { + var body = expression.Body as MemberExpression; + if (body is null) + { + throw new ArgumentException(ExpressionNotRepresentableMessage); + } + + // Handles cases like @(x => x.Name) + if (body.Expression is ParameterExpression) + { + return body.Member.Name; + } + + // First work out the length of the string we'll need, so that we can use string.Create + var length = body.Member.Name.Length; + var node = body; + while (node.Expression is not null) + { + if (node.Expression is MemberExpression parentMember) + { + length += parentMember.Member.Name.Length + 1; + node = parentMember; + } + else if (node.Expression is ParameterExpression) + { + break; + } + else + { + throw new ArgumentException(ExpressionNotRepresentableMessage); + } + } + + // Now construct the string + return string.Create(length, body, (chars, body) => + { + var nextPos = chars.Length; + while (body is not null) + { + nextPos -= body.Member.Name.Length; + body.Member.Name.CopyTo(chars.Slice(nextPos)); + if (nextPos > 0) + { + chars[--nextPos] = '.'; + } + body = (body.Expression as MemberExpression)!; + } + }); + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridTemplateColumn.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridTemplateColumn.cs new file mode 100644 index 0000000000..f7b17bda4d --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/BitDataGridTemplateColumn.cs @@ -0,0 +1,30 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a column whose cells render a supplied template. +/// +/// The type of data represented by each row in the grid. +public class BitDataGridTemplateColumn : BitDataGridColumnBase, IBitDataGridSortBuilderColumn +{ + private readonly static RenderFragment EmptyChildContent = _ => builder => { }; + + /// + /// Specifies the content to be rendered for each row in the table. + /// + [Parameter] public RenderFragment ChildContent { get; set; } = EmptyChildContent; + + /// + /// Optionally specifies sorting rules for this column. + /// + [Parameter] public BitDataGridSort? SortBy { get; set; } + + BitDataGridSort? IBitDataGridSortBuilderColumn.SortBuilder => SortBy; + + /// + protected internal override void CellContent(RenderTreeBuilder builder, TGridItem item) + => builder.AddContent(0, ChildContent(item)); + + /// + protected override bool IsSortableByDefault() + => SortBy is not null; +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/IBitDataGridSortBuilderColumn.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/IBitDataGridSortBuilderColumn.cs new file mode 100644 index 0000000000..a57c882ac4 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Columns/IBitDataGridSortBuilderColumn.cs @@ -0,0 +1,18 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// An interface that, if implemented by a subclass, allows a +/// to understand the sorting rules associated with that column. +/// +/// If a subclass does not implement this, that column can still be marked as sortable and can +/// be the current sort column, but its sorting logic cannot be applied to the data queries automatically. The developer would be +/// responsible for implementing that sorting logic separately inside their . +/// +/// The type of data represented by each row in the grid. +public interface IBitDataGridSortBuilderColumn +{ + /// + /// Gets the sorting rules associated with the column. + /// + public BitDataGridSort? SortBuilder { get; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/AsyncQueryExecutorSupplier.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/AsyncQueryExecutorSupplier.cs new file mode 100644 index 0000000000..1291e213cf --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/AsyncQueryExecutorSupplier.cs @@ -0,0 +1,68 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.BlazorUI.Legacy; + +internal static class AsyncQueryExecutorSupplier +{ + // The primary goal with this is to ensure that: + // - If you're using EF Core, then we resolve queries efficiently using its ToXyzAsync async extensions and don't + // just fall back on the synchronous IQueryable ToXyz calls + // - ... but without BitDataGrid referencing Microsoft.EntityFramework directly. That's because it would bring in + // heavy dependencies you may not be using (and relying on trimming isn't enough, as it's still desirable to have + // heavy unused dependencies for Blazor Server). + // + // As a side-effect, we have an abstraction IAsyncQueryExecutor that developers could use to plug in their own + // mechanism for resolving async queries from other data sources than EF. It's not really a major goal to make this + // adapter generally useful beyond EF, but fine if people do have their own uses for it. + + private static readonly ConcurrentDictionary IsEntityFrameworkProviderTypeCache = new(); + + [SuppressMessage("Trimming", "IL2111:Method with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the method.", Justification = "")] + public static IAsyncQueryExecutor? GetAsyncQueryExecutor(IServiceProvider services, IQueryable? queryable) + { + if (queryable is not null) + { + // Inspect every registered executor, not just the first one resolved: a registered executor + // that does not support this queryable must not shadow another that does, nor suppress the + // EF misconfiguration warning below. Keep scanning and return the *last* supported executor + // so later, more specific registrations override earlier generic ones - mirroring how DI + // resolves a single service (last registration wins). + IAsyncQueryExecutor? selected = null; + foreach (var executor in services.GetServices()) + { + if (executor.IsSupported(queryable)) + { + selected = executor; + } + } + + if (selected is not null) + { + return selected; + } + + // No registered executor supports this queryable. It's useful to detect if the developer is + // unaware that they should register an IAsyncQueryExecutor, otherwise they will likely never + // notice and simply deploy an inefficient app that blocks threads on each query. + var providerType = queryable.Provider?.GetType(); + if (providerType is not null && IsEntityFrameworkProviderTypeCache.GetOrAdd(providerType, IsEntityFrameworkProviderType)) + { + throw new InvalidOperationException( + $"The supplied {nameof(IQueryable)} is provided by Entity Framework. To query it efficiently without blocking threads, " + + $"register an implementation of {nameof(IAsyncQueryExecutor)} in your service collection that wraps EF Core's async query APIs " + + $"(for example ToArrayAsync/CountAsync) and reports IsSupported(queryable) == true for EF queryables. " + + $"Alternatively, supply non-EF data via the Items or ItemsProvider parameters."); + } + } + + return null; + } + + // We have to do this via reflection because the whole point is to avoid any static dependency on EF unless you + // reference the adapter. Trimming won't cause us any problems because this is only a way of detecting misconfiguration + // so it's sufficient if it can detect the misconfiguration in development. + private static bool IsEntityFrameworkProviderType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type queryableProviderType) + => queryableProviderType.GetInterfaces().Any(x => string.Equals(x.FullName, "Microsoft.EntityFrameworkCore.Query.IAsyncQueryProvider")) == true; +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/BitDataGridColumnsCollectedNotifier.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/BitDataGridColumnsCollectedNotifier.cs new file mode 100644 index 0000000000..d98b75caa4 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/BitDataGridColumnsCollectedNotifier.cs @@ -0,0 +1,54 @@ +namespace Bit.BlazorUI.Legacy; + +// One awkwardness of the way BitDataGrid collects its list of child columns is that, during OnParametersSetAsync, +// it only knows about the set of columns that were present on the *previous* render. If it's going to trigger a +// data load during OnParametersSetAsync, that operation can't depend on the current set of columns as it might +// have changed, or might even still be empty (i.e., on the first render). +// +// Ways this could be resolved: +// +// - In the future, we could implement the long-wanted feature of being able to query the contents of a RenderFragment +// separately from rendering. Then the whole trick of collection-during-rendering would not be needed. +// - Or, we could factor out most of BitDataGrid's internals into some new component BitDataGridCore. The parent component, +// BitDataGrid, would then only be responsible for collecting columns followed by rendering BitDataGridCore. So each time +// BitDataGridCore renders, we'd already have the latest set of columns +// - Drawback: since BitDataGrid has public API, it's much messier to have to forward all of that to some new child type. +// - However, this is arguably the most correct solution in general (at least until option 1 above is implemented) +// - Or, we could decide it's enough to fix this on the first render (since that's the only time we're going to guarantee +// to apply a default sort order), and then as a special case put in some extra component in the render flow that raises +// an event once the columns are first collected. +// - This is relatively simple and non-disruptive, though it doesn't cover cases where queries need to be delayed until +// after a dynamically-added column is added +// +// The final option is what's implemented here. We send the notification via EventCallbackSubscribable so that the async +// operation and re-rendering follows normal semantics without us having to call StateHasChanged or think about exceptions. + +/// +/// For internal use only. Do not use. +/// +/// For internal use only. Do not use. +public class BitDataGridColumnsCollectedNotifier : IComponent +{ + private bool _isFirstRender = true; + + [CascadingParameter] internal InternalGridContext InternalGridContext { get; set; } = default!; + + public void Attach(RenderHandle renderHandle) + { + // This component never renders, so we can ignore the renderHandle + } + + public Task SetParametersAsync(ParameterView parameters) + { + if (_isFirstRender) + { + _isFirstRender = false; + parameters.SetParameterProperties(this); + return InternalGridContext.ColumnsFirstCollected.InvokeCallbacksAsync(null); + } + else + { + return Task.CompletedTask; + } + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/BitDataGridDefer.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/BitDataGridDefer.cs new file mode 100644 index 0000000000..ad0066315b --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/BitDataGridDefer.cs @@ -0,0 +1,23 @@ +namespace Bit.BlazorUI.Legacy; + +// This is used by BitDataGrid to move its body rendering to the end of the render queue so we can collect +// the list of child columns first. It has to be public only because it's used from .razor logic. + +/// +/// For internal use only. Do not use. +/// +public class BitDataGridDefer : ComponentBase +{ + /// + /// For internal use only. Do not use. + /// + [Parameter] public RenderFragment? ChildContent { get; set; } + + /// + /// For internal use only. Do not use. + /// + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, ChildContent); + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/EventCallbackSubscribable.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/EventCallbackSubscribable.cs new file mode 100644 index 0000000000..473c87b7d4 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/EventCallbackSubscribable.cs @@ -0,0 +1,31 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents an event that you may subscribe to. This differs from normal C# events in that the handlers +/// are EventCallback, and so may have async behaviors and cause component re-rendering +/// while retaining error flow. +/// +/// A type for the eventargs. +internal class EventCallbackSubscribable +{ + private readonly Dictionary, EventCallback> _callbacks = new(); + + /// + /// Invokes all the registered callbacks sequentially, in an undefined order. + /// + public async Task InvokeCallbacksAsync(T eventArg) + { + foreach (var callback in _callbacks.Values) + { + await callback.InvokeAsync(eventArg); + } + } + + // Don't call this directly - it gets called by EventCallbackSubscription + public void Subscribe(EventCallbackSubscriber owner, EventCallback callback) + => _callbacks.Add(owner, callback); + + // Don't call this directly - it gets called by EventCallbackSubscription + public void Unsubscribe(EventCallbackSubscriber owner) + => _callbacks.Remove(owner); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/EventCallbackSubscriber.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/EventCallbackSubscriber.cs new file mode 100644 index 0000000000..935c960143 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/EventCallbackSubscriber.cs @@ -0,0 +1,41 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Represents a subscriber that may be subscribe to an . +/// The subscription can move between instances over time, +/// and automatically unsubscribes from earlier instances +/// whenever it moves to a new one. +/// +internal class EventCallbackSubscriber : IDisposable +{ + private readonly EventCallback _handler; + private EventCallbackSubscribable? _existingSubscription; + + public EventCallbackSubscriber(EventCallback handler) + { + _handler = handler; + } + + /// + /// Creates a subscription on the , or moves any existing subscription to it + /// by first unsubscribing from the previous . + /// + /// If the supplied is null, no new subscription will be created, but any + /// existing one will still be unsubscribed. + /// + /// + public void SubscribeOrMove(EventCallbackSubscribable? subscribable) + { + if (subscribable != _existingSubscription) + { + _existingSubscription?.Unsubscribe(this); + subscribable?.Subscribe(this, _handler); + _existingSubscription = subscribable; + } + } + + public void Dispose() + { + _existingSubscription?.Unsubscribe(this); + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/EventHandlers.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/EventHandlers.cs new file mode 100644 index 0000000000..9f9bb50757 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/EventHandlers.cs @@ -0,0 +1,9 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Configures event handlers for . +/// +[EventHandler("onclosecolumnoptions", typeof(EventArgs), enableStopPropagation: true, enablePreventDefault: true)] +public static class EventHandlers +{ +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/IAsyncQueryExecutor.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/IAsyncQueryExecutor.cs new file mode 100644 index 0000000000..f18d884e71 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/IAsyncQueryExecutor.cs @@ -0,0 +1,31 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Provides methods for asynchronous evaluation of queries against an . +/// +public interface IAsyncQueryExecutor +{ + /// + /// Determines whether the is supported by this type. + /// + /// The data type. + /// An instance. + /// True if this instance can perform asynchronous queries for the supplied , otherwise false. + bool IsSupported(IQueryable queryable); + + /// + /// Asynchronously counts the items in the , if supported. + /// + /// The data type. + /// An instance. + /// The number of items in .. + Task CountAsync(IQueryable queryable); + + /// + /// Asynchronously materializes the as an array, if supported. + /// + /// The data type. + /// An instance. + /// The items in the .. + Task ToArrayAsync(IQueryable queryable); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/InternalGridContext.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/InternalGridContext.cs new file mode 100644 index 0000000000..496aeb190d --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Infrastructure/InternalGridContext.cs @@ -0,0 +1,14 @@ +namespace Bit.BlazorUI.Legacy; + +// The grid cascades this so that descendant columns can talk back to it. It's an internal type +// so that it doesn't show up by mistake in unrelated components. +internal class InternalGridContext +{ + public BitDataGrid Grid { get; } + public EventCallbackSubscribable ColumnsFirstCollected { get; } = new(); + + public InternalGridContext(BitDataGrid grid) + { + Grid = grid; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/ItemsProvider/BitDataGridItemsProvider.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/ItemsProvider/BitDataGridItemsProvider.cs new file mode 100644 index 0000000000..268f9a1c93 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/ItemsProvider/BitDataGridItemsProvider.cs @@ -0,0 +1,9 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// A callback that provides data for a . +/// +/// The type of data represented by each row in the grid. +/// Parameters describing the data being requested. +/// A (specifically ValueTask<BitDataGridItemsProviderResult<TGridItem>>) whose result is a that gives the data to be displayed. +public delegate ValueTask> BitDataGridItemsProvider(BitDataGridItemsProviderRequest request); diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/ItemsProvider/BitDataGridItemsProviderRequest.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/ItemsProvider/BitDataGridItemsProviderRequest.cs new file mode 100644 index 0000000000..781b66516c --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/ItemsProvider/BitDataGridItemsProviderRequest.cs @@ -0,0 +1,91 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Parameters for data to be supplied by a 's . +/// +/// The type of data represented by each row in the grid. +public struct BitDataGridItemsProviderRequest +{ + /// + /// The zero-based index of the first item to be supplied. + /// + public int StartIndex { get; } + + /// + /// If set, the maximum number of items to be supplied. If not set, the maximum number is unlimited. + /// + public int? Count { get; } + + /// + /// Specifies which column represents the sort order. + /// + /// Rather than inferring the sort rules manually, you should normally call either + /// or , since they also account for and automatically. + /// + public BitDataGridColumnBase? SortByColumn { get; } + + /// + /// Specifies the current sort direction. + /// + /// Rather than inferring the sort rules manually, you should normally call either + /// or , since they also account for and automatically. + /// + public bool SortByAscending { get; } + + /// + /// A token that indicates if the request should be cancelled. + /// + public CancellationToken CancellationToken { get; } + + internal BitDataGridItemsProviderRequest( + int startIndex, int? count, BitDataGridColumnBase? sortByColumn, bool sortByAscending, + CancellationToken cancellationToken) + { + StartIndex = startIndex; + Count = count; + SortByColumn = sortByColumn; + SortByAscending = sortByAscending; + CancellationToken = cancellationToken; + } + + /// + /// Applies the request's sorting rules to the supplied . + /// + /// Note that this only works if the current implements + /// and exposes a non-null sort builder. If the column does not implement that interface, or implements it + /// but its sort builder is null (as with ), it will throw. + /// + /// An . + /// A new representing the with sorting rules applied. + public IQueryable ApplySorting(IQueryable source) => SortByColumn switch + { + // A sort-builder column with a null SortBuilder cannot apply its sort; treat it as unsupported + // (like a non-sort-builder column) rather than silently returning the unsorted source, which + // would hide an active sort on e.g. BitDataGridTemplateColumn. + IBitDataGridSortBuilderColumn { SortBuilder: { } sortBuilder } => sortBuilder.Apply(source, SortByAscending), + null => source, + _ => throw new NotSupportedException(ColumnNotSortableMessage(SortByColumn)), + }; + + /// + /// Produces a collection of (property name, direction) pairs representing the sorting rules. + /// + /// Note that this only works if the current implements + /// and exposes a non-null sort builder. If the column does not implement that interface, or implements it + /// but its sort builder is null (as with ), it will throw. + /// + /// A collection of (property name, direction) pairs representing the sorting rules + public IReadOnlyCollection<(string PropertyName, BitDataGridSortDirection Direction)> GetSortByProperties() => SortByColumn switch + { + // Mirror ApplySorting: a null SortBuilder on a sort-builder column is unsupported rather than + // an empty (no-op) sort, so the caller isn't misled into thinking there is no active sort. + IBitDataGridSortBuilderColumn { SortBuilder: { } sortBuilder } => sortBuilder.ToPropertyList(SortByAscending), + null => Array.Empty<(string, BitDataGridSortDirection)>(), + _ => throw new NotSupportedException(ColumnNotSortableMessage(SortByColumn)), + }; + + private static string ColumnNotSortableMessage(BitDataGridColumnBase col) + => col is IBitDataGridSortBuilderColumn + ? $"The current sort column '{col.GetType().FullName}' implements {nameof(IBitDataGridSortBuilderColumn)} but its {nameof(IBitDataGridSortBuilderColumn.SortBuilder)} is null, so its sorting rules cannot be applied automatically." + : $"The current sort column is of type '{col.GetType().FullName}', which does not implement {nameof(IBitDataGridSortBuilderColumn)}, so its sorting rules cannot be applied automatically."; +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/ItemsProvider/BitDataGridItemsProviderResult.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/ItemsProvider/BitDataGridItemsProviderResult.cs new file mode 100644 index 0000000000..e3765569d1 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/ItemsProvider/BitDataGridItemsProviderResult.cs @@ -0,0 +1,49 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Holds data being supplied to a 's . +/// +/// The type of data represented by each row in the grid. +public struct BitDataGridItemsProviderResult +{ + /// + /// The items being supplied. + /// + public ICollection Items { get; set; } + + /// + /// The total number of items that may be displayed in the grid. This normally means the total number of items in the + /// underlying data source after applying any filtering that is in effect. + /// + /// If the grid is paginated, this should include all pages. If the grid is virtualized, this should include the entire scroll range. + /// + public int TotalItemCount { get; set; } + + /// + /// Constructs an instance of . + /// + /// The items being supplied. + /// The total number of items that exist. See for details. + public BitDataGridItemsProviderResult(ICollection items, int totalItemCount) + { + Items = items; + TotalItemCount = totalItemCount; + } +} + +/// +/// Provides convenience methods for constructing instances. +/// +public static class BitDataGridItemsProviderResult +{ + // This is just to provide generic type inference, so you don't have to specify TGridItem yet again. + + /// + /// Supplies an instance of . + /// + /// The type of data represented by each row in the grid. + /// The items being supplied. + /// The total number of items that exist. See for details. + /// An instance of . + public static BitDataGridItemsProviderResult From(ICollection items, int totalItemCount) => new(items, totalItemCount); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Pagination/BitDataGridPaginationState.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Pagination/BitDataGridPaginationState.cs new file mode 100644 index 0000000000..dee8839351 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Pagination/BitDataGridPaginationState.cs @@ -0,0 +1,76 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// Holds state to represent pagination in a . +/// +public class BitDataGridPaginationState +{ + /// + /// Gets the current zero-based page index. To set it, call . + /// + public int CurrentPageIndex { get; private set; } + + /// + /// Gets or sets the number of items on each page. + /// + public int ItemsPerPage { get; set; } = 10; + + /// + /// Gets the zero-based index of the last page, if known. The value will be null until is known. + /// + public int? LastPageIndex => (TotalItemCount - 1) / ItemsPerPage; + + /// + /// Gets the total number of items across all pages, if known. The value will be null until an + /// associated assigns a value after loading data. + /// + public int? TotalItemCount { get; private set; } + + /// + /// An event that is raised when the total item count has changed. + /// + public event EventHandler? TotalItemCountChanged; + + internal EventCallbackSubscribable CurrentPageItemsChanged { get; } = new(); + internal EventCallbackSubscribable TotalItemCountChangedSubscribable { get; } = new(); + + /// + public override int GetHashCode() + => HashCode.Combine(ItemsPerPage, CurrentPageIndex, TotalItemCount); + + /// + /// Sets the current page index, and notifies any associated + /// to fetch and render updated data. + /// + /// The new, zero-based page index. + /// A representing the completion of the operation. + public Task SetCurrentPageIndexAsync(int pageIndex) + { + CurrentPageIndex = pageIndex; + return CurrentPageItemsChanged.InvokeCallbacksAsync(this); + } + + // Can be internal because this only needs to be called by BitDataGrid itself, not any custom pagination UI components. + internal Task SetTotalItemCountAsync(int totalItemCount) + { + if (totalItemCount == TotalItemCount) + { + return Task.CompletedTask; + } + + TotalItemCount = totalItemCount; + + if (CurrentPageIndex > 0 && CurrentPageIndex > LastPageIndex) + { + // If the number of items has reduced such that the current page index is no longer valid, move + // automatically to the final valid page index and trigger a further data load. + return SetCurrentPageIndexAsync(LastPageIndex.Value); + } + else + { + // Under normal circumstances, we just want any associated pagination UI to update + TotalItemCountChanged?.Invoke(this, TotalItemCount); + return TotalItemCountChangedSubscribable.InvokeCallbacksAsync(this); + } + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Pagination/BitDataGridPaginator.razor b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Pagination/BitDataGridPaginator.razor new file mode 100644 index 0000000000..c1e7f479bc --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Pagination/BitDataGridPaginator.razor @@ -0,0 +1,61 @@ +@namespace Bit.BlazorUI.Legacy + +
+ @if (Value.TotalItemCount.HasValue) + { +
+ @if (SummaryTemplate is not null) + { + @SummaryTemplate(Value) + } + else if (SummaryFormat is not null) + { + @SummaryFormat(Value) + } + else + { + @Value.TotalItemCount items + } +
+ + } +
diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Pagination/BitDataGridPaginator.razor.cs b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Pagination/BitDataGridPaginator.razor.cs new file mode 100644 index 0000000000..dd8555f616 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Pagination/BitDataGridPaginator.razor.cs @@ -0,0 +1,86 @@ +namespace Bit.BlazorUI.Legacy; + +/// +/// A component that provides a user interface for . +/// +public partial class BitDataGridPaginator : IDisposable +{ + private readonly EventCallbackSubscriber _totalItemCountChanged; + + /// + /// The title of the go to first page button. + /// + [Parameter] public string GoToFirstButtonTitle { get; set; } = "Go to first page"; + + /// + /// The title of the go to previous page button. + /// + [Parameter] public string GoToPrevButtonTitle { get; set; } = "Go to previous page"; + + /// + /// The title of the go to next page button. + /// + [Parameter] public string GoToNextButtonTitle { get; set; } = "Go to next page"; + + /// + /// The title of the go to last page button. + /// + [Parameter] public string GoToLastButtonTitle { get; set; } = "Go to last page"; + + /// + /// Optionally supplies a format for rendering the page count summary. + /// + [Parameter] public Func? SummaryFormat { get; set; } + + /// + /// Optionally supplies a template for rendering the page count summary. + /// + [Parameter] public RenderFragment? SummaryTemplate { get; set; } + + /// + /// The optional custom format for the main text of the paginator in the middle of it. + /// + [Parameter] public Func? TextFormat { get; set; } + + /// + /// The optional custom template for the main text of the paginator in the middle of it. + /// + [Parameter] public RenderFragment? TextTemplate { get; set; } + + /// + /// Specifies the associated . This parameter is required. + /// + [Parameter, EditorRequired] public BitDataGridPaginationState Value { get; set; } = default!; + + /// + /// Constructs an instance of . + /// + public BitDataGridPaginator() + { + // The "total item count" handler doesn't need to do anything except cause this component to + // re-render. Invoking this EventCallback already routes through the paginator's + // IHandleEvent.HandleEventAsync (the receiver is `this`), which re-renders the component on its + // own, so the callback body is intentionally empty - calling StateHasChanged() here as well + // would queue a second, redundant render. + _totalItemCountChanged = new(EventCallback.Factory.Create(this, () => { })); + } + + private Task GoFirstAsync() => GoToPageAsync(0); + private Task GoPreviousAsync() => GoToPageAsync(Value.CurrentPageIndex - 1); + private Task GoNextAsync() => GoToPageAsync(Value.CurrentPageIndex + 1); + private Task GoLastAsync() => GoToPageAsync(Value.LastPageIndex.GetValueOrDefault(0)); + + private bool CanGoBack => Value.CurrentPageIndex > 0; + private bool CanGoForwards => Value.CurrentPageIndex < Value.LastPageIndex; + + private Task GoToPageAsync(int pageIndex) + => Value.SetCurrentPageIndexAsync(pageIndex); + + /// + protected override void OnParametersSet() + => _totalItemCountChanged.SubscribeOrMove(Value.TotalItemCountChangedSubscribable); + + /// + public void Dispose() + => _totalItemCountChanged.Dispose(); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Pagination/BitDataGridPaginator.scss b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Pagination/BitDataGridPaginator.scss new file mode 100644 index 0000000000..5d72b85b01 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Pagination/BitDataGridPaginator.scss @@ -0,0 +1,59 @@ +.bit-dgp { + display: flex; + flex-wrap: wrap; + padding: 0.25rem; + align-items: center; +} + +.bit-dgp-txt { + margin: 0 0.5rem; + text-align: center; +} + +.bit-dgp-nav { + gap: 0.5rem; + display: flex; + align-items: center; + margin-inline-start: auto; + + button { + border: 0; + width: 2rem; + height: 2rem; + background: none center center / 1rem no-repeat; + + &[disabled] { + opacity: 0.4; + } + + &:not([disabled]):hover { + background-color: var(--bit-clr-bg-pri-hover); + } + + &:not([disabled]):active { + background-color: var(--bit-clr-bg-pri-active); + } + } +} + +.bit-dgp-fst::before, +.bit-dgp-lst::before { + content: "\F371"; + font-style: normal; + font-weight: normal; + display: inline-block; + font-family: 'Fabric MDL2 bit BlazorUI Extras'; +} + +.bit-dgp-prv::before, +.bit-dgp-nex::before { + content: "\E96F"; + font-style: normal; + font-weight: normal; + display: inline-block; + font-family: 'Fabric MDL2 bit BlazorUI Extras'; +} + +.bit-dgp-nex, .bit-dgp-lst { + transform: scaleX(-1); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/MarkdownEditor/BitMarkdownEditor.razor b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/MarkdownEditor/BitMarkdownEditor.razor new file mode 100644 index 0000000000..533b502125 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/MarkdownEditor/BitMarkdownEditor.razor @@ -0,0 +1,11 @@ +@namespace Bit.BlazorUI.Legacy +@inherits BitComponentBase + +
+