A Blazor component designed to provide an interactive image magnification experience, similar to popular eCommerce websites.
MagnifyScale parameter.@inject IJSRuntime JS
@using Microsoft.JSInterop
@* Container *@
<div @ref="_containerRef" class="@($"magnifiable-image-container {Class}")" style="height: @Height; width: @Width;">
@* Image *@
<button @onclick="@OnClick" class="magnifiable-image-button">
<img src="@Image.Src" alt="@Image.Alt" style="height: 100%; width: 100%;"
@onmousemove="@OnMouseMove" @onmouseenter="@OnMouseEnterAsync" @onmouseleave="@OnMouseLeave"/>
</button>
@* Magnifier *@
<TelerikPopover @ref="_popoverRef" AnchorSelector=".magnifiable-image-container" Position="@(_showOnRight ? PopoverPosition.Right : PopoverPosition.Left)" Offset="@MagnifierMargin"
Width="@($"{_magnifierWidth}px")" Height="@($"{_magnifierHeight}px")" Class="popover-magnifier" Collision="PopoverCollision.Fit">
<PopoverContent>
@* Magnified Image *@
<img src="@Image.Src" alt="@Image.Alt" class="magnified-image"
style="@($"width: {_magnifiedImageWidth}px; height: {_magnifiedImageHeight}px; transform: translateX({_magnifiedImageTransformX}px) translateY({_magnifiedImageTransformY}px); left: {_magnifiedImageLeft}px; top: {_magnifiedImageTop}px;")"/>
</PopoverContent>
</TelerikPopover>
@* Magnifier Overlay *@
@if (_isMouseOver)
{
<div class="magnifier-overlay"
style="@($"width: {_magnifierOverlayWidth}px; height: {_magnifierOverlayHeight}px; transform: translateX({_magnifierOverlayTransformX}px) translateY({_magnifierOverlayTransformY}px); left: {_magnifierOverlayLeft}px; top: {_magnifierOverlayTop}px;")))">
</div>
}
@* Actual Image *@
<TelerikWindow @bind-Visible="@_isClicked" Modal="true" CloseOnOverlayClick="true" Draggable="false" Resizable="false" Class="window-rounded">
<WindowActions>
<WindowAction Name="Close"/>
</WindowActions>
<WindowContent>
<img src="@Image.Src" alt="@Image.Alt"/>
</WindowContent>
</TelerikWindow>
</div>
<style>
.magnifiable-image-container {
position: relative;
display: inline-block;
cursor: zoom-in;
}
.magnifiable-image-button {
background: none;
border: none;
padding: 0;
margin: 0;
font: inherit;
color: inherit;
cursor: inherit !important;
outline: none;
box-shadow: none;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
display: block;
width: 100%;
height: 100%;
}
.magnifiable-image-button:focus-visible {
outline: none;
box-shadow: 0 0 0 2px color-mix(in srgb, var(--kendo-color-on-app-surface, #424242) 50%, transparent);
}
.popover-magnifier {
overflow: hidden;
border-radius: 0;
position: relative;
top: @(_adjustForTelerikFit ? $"{(_showOnBottom ? "" : "-")}{MagnifierMargin/4}px" : $"{(_showOnBottom ? "" : "-")}{MagnifierMargin}px");
}
.magnified-image {
position: absolute;
}
.magnifier-overlay {
position: absolute;
background: color-mix(in srgb, var(--kendo-color-primary, #1274AC) 15%, transparent);
pointer-events: none;
z-index: 5;
box-sizing: border-box;
display: block;
}
</style>
@code
{
[Parameter] public required ImageInfo Image { get; set; }
/// <summary>
/// The height of the image (e.g., "200px", "100%", or "auto"). Default is "auto".
/// </summary>
[Parameter]
public string Height { get; set; } = "auto";
/// <summary>
/// The width of the image (e.g., "200px", "100%", or "auto"). Default is "auto".
/// </summary>
[Parameter]
public string Width { get; set; } = "auto";
/// <summary>
/// The magnification scale for the magnifier. Default is 3 (3x magnification).
/// </summary>
[Parameter]
public double MagnifyScale { get; set; } = 3;
/// <summary>
/// Applies additional CSS classes to the MagnifiableImage's root element for custom styling and visual modifications.
/// </summary>
[Parameter]
public string Class { get; set; } = string.Empty;
private const int MagnifierMargin = 24;
// State for magnifier visibility and container reference
private bool _isMouseOver;
private bool _isClicked;
private ElementReference _containerRef;
private TelerikPopover? _popoverRef;
// Image and magnified image dimensions
private double _imageWidth;
private double _imageHeight;
private double _magnifiedImageWidth;
private double _magnifiedImageHeight;
// Magnifier position and size
private bool _showOnRight = true;
private bool _showOnBottom = true;
private bool _adjustForTelerikFit;
private double _magnifierWidth;
private double _magnifierHeight;
// Magnified image offset within the magnifier
private double _magnifiedImageLeft;
private double _magnifiedImageTop;
// Mouse position clamping bounds
private double _minMouseX;
private double _minMouseY;
private double _maxMouseX;
private double _maxMouseY;
// Mouse position and transform for magnified image
private double _mouseX;
private double _mouseY;
private double _magnifiedImageTransformX;
private double _magnifiedImageTransformY;
// Magnifier overlay size and transform
private double _magnifierOverlayWidth;
private double _magnifierOverlayHeight;
private double _magnifierOverlayLeft;
private double _magnifierOverlayTop;
private double _magnifierOverlayTransformX;
private double _magnifierOverlayTransformY;
private void OnClick()
{
_isClicked = true;
}
private async Task OnMouseEnterAsync()
{
_isMouseOver = true;
_popoverRef?.Show();
// Get layout info about the image container using getElementLayoutInfo from wwwroot/js/magnifiable-image.js
var containerLayoutInfo = await JS.InvokeAsync<ElementLayoutInfo>("getElementLayoutInfo", _containerRef);
// Store image size
_imageWidth = containerLayoutInfo.Width;
_imageHeight = containerLayoutInfo.Height;
_magnifiedImageWidth = _imageWidth * MagnifyScale;
_magnifiedImageHeight = _imageHeight * MagnifyScale;
// Determine magnifier position based on available space
_showOnRight = containerLayoutInfo.DistanceFromViewportRight >= containerLayoutInfo.DistanceFromViewportLeft;
_adjustForTelerikFit = Math.Abs(containerLayoutInfo.DistanceFromViewportBottom - containerLayoutInfo.DistanceFromViewportTop) < 20;
_showOnBottom = containerLayoutInfo.DistanceFromViewportBottom >= containerLayoutInfo.DistanceFromViewportTop;
// Calculate magnifier size based on available space
_magnifierWidth = _showOnRight
? containerLayoutInfo.DistanceFromViewportRight - MagnifierMargin*2
: containerLayoutInfo.DistanceFromViewportLeft - MagnifierMargin*2;
_magnifierHeight = containerLayoutInfo.ViewportHeight - MagnifierMargin*2;
// Center the magnified image in the magnifier
_magnifiedImageLeft = (_magnifierWidth / 2) - (_imageWidth / 2);
_magnifiedImageTop = (_magnifierHeight / 2) - (_imageHeight / 2);
// Calculate min and max mouse X/Y to prevent showing empty space in the magnifier
_minMouseX = Math.Floor((_magnifierWidth / MagnifyScale) / 2);
_minMouseY = Math.Floor((_magnifierHeight / MagnifyScale) / 2);
_maxMouseX = Math.Ceiling(_imageWidth - ((_magnifierWidth / MagnifyScale) / 2));
_maxMouseY = Math.Ceiling(_imageHeight - ((_magnifierHeight / MagnifyScale) / 2));
// Calculate magnifier overlay size and position
_magnifierOverlayWidth = Math.Floor(Math.Clamp(_magnifierWidth / MagnifyScale, 0, _imageWidth)) - 1;
_magnifierOverlayHeight = Math.Floor(Math.Clamp(_magnifierHeight / MagnifyScale, 0, _imageHeight)) - 1;
_magnifierOverlayLeft = -((_magnifierOverlayWidth / 2) + 1);
_magnifierOverlayTop = -((_magnifierOverlayHeight / 2) + 1);
}
private void OnMouseLeave()
{
_isMouseOver = false;
_popoverRef?.Hide();
}
private void OnMouseMove(MouseEventArgs e)
{
// Clamp mouse X/Y to prevent showing empty space in the magnifier
if (_minMouseX > _maxMouseX) _mouseX = _imageWidth / 2;
else _mouseX = Math.Clamp(e.OffsetX, _minMouseX, _maxMouseX);
if (_minMouseY > _maxMouseY) _mouseY = _imageHeight / 2;
else _mouseY = Math.Clamp(e.OffsetY, _minMouseY, _maxMouseY);
// Calculate the transform for the magnified image
_magnifiedImageTransformX = -Math.Round((_mouseX * MagnifyScale) - (_imageWidth / 2));
_magnifiedImageTransformY = -Math.Round((_mouseY * MagnifyScale) - (_imageHeight / 2));
// Calculate the transform for the magnifier overlay
_magnifierOverlayTransformX = Math.Round(_mouseX);
_magnifierOverlayTransformY = Math.Round(_mouseY);
_popoverRef?.Refresh();
}
private record ElementLayoutInfo(
double Width,
double Height,
double ViewportHeight,
double DistanceFromViewportLeft, // distance from viewport's left edge to element's left edge
double DistanceFromViewportRight, // distance from element's right edge to viewport's right edge
double DistanceFromViewportTop, // distance from viewport's top edge to element's top edge
double DistanceFromViewportBottom); // distance from element's bottom edge to viewport's bottom edge
}// Returns width, height, and the space to the left and right of the element relative to the viewport
window.getElementLayoutInfo = (element) => {
if (!element) return null;
const elementRect = element.getBoundingClientRect();
return {
width: elementRect.width,
height: elementRect.height,
viewportHeight: window.innerHeight,
distanceFromViewportLeft: elementRect.left, // distance from viewport's left edge to element's left edge
distanceFromViewportRight: window.innerWidth - elementRect.right, // distance from element's right edge to viewport's right edge
distanceFromViewportTop: elementRect.top, // distance from viewport's top edge to element's top edge
distanceFromViewportBottom: window.innerHeight - elementRect.bottom // distance from element's bottom edge to viewport's bottom edge
};
};Only tested in Blazor WebAssembly. The component may see performance issues in Blazor Server.
Please expose the popup collision settings of the Popup and Popover components to all other applicable components, such as:
I realize we can build Blazor components to associate "label" to controls, but IMHO, this should come standard with any control tool set. Reasons:
1. Coding simplicity
2. Automatic theme inheritance
3. Flexibility in label positioning relative to the control (left, right, top, bottom)
Example:
<TelerikCheckBox Label="Is Chasssis" LabelPosition="left" @bind-Value="equipment.IsChassis"/>
I realize you folks put some effort into a "Floating Label", but my users have rejected this implementation because:
1. Having Text in a label as a means to identify the label makes them think a value is already provided (so they try to remove it or call for support).
2. When typing in the label and the appearance of the label identification appears above adds to their confusion as they are used to seeing validation errors appearing above a label.
Affected components: those inheriting from TelerikInputBase (e.g., TelerikDatePicker). When an exception is thrown inside an async Task event handler for the OnChange, OnBlur, OnOpen, and ValueChanged events, the exception is completely and silently swallowed. The exception is not caught by ErrorBoundary.
<ErrorBoundary>
<ChildContent>
@Body
</ChildContent>
<ErrorContent>
<p class="error">An unhandled error has occurred.</p>
</ErrorContent>
</ErrorBoundary>
<TelerikDatePicker Value="@DatePickerValue"
ValueChanged="@((DateTime inputDate) => OnDatePickerValueChanged(inputDate))">
</TelerikDatePicker>
<TelerikButton OnClick="@(() => throw new Exception("Exception from button"))">Click to test ErrorBoundary</TelerikButton>
@code {
private DateTime DatePickerValue { get; set; } = DateTime.Today;
private async Task OnDatePickerValueChanged(DateTime newValue)
{
throw new InvalidOperationException("This exception should be caught by the ErrorBoundary!");
}
}
The exception thrown in the OnDatePickerValueChanged event handler is not caught by ErrorBoundary.
The exception thrown in the OnDatePickerValueChanged event handler is caught by ErrorBoundary.
All
No response
Affected components: those inheriting from TelerikSelectBase (e.g., TelerikDropDownList, TelerikComboBox, TelerikMultiSelect, TelerikAutoComplete). When an exception is thrown inside an async Task event handler for the OnChange, OnBlur, OnOpen, and ValueChanged events, the exception is completely and silently swallowed. The exception is not caught by ErrorBoundary.
Related: #6333
Steps to Reproduce
<ErrorBoundary>
<ChildContent>
@Body
</ChildContent>
<ErrorContent>
<p class="error">An unhandled error has occurred.</p>
</ErrorContent>
</ErrorBoundary>
<TelerikDropDownList
Data="@DropDownData"
ValueChanged="@( (int newValue) => OnDropDownValueChanged(newValue))"
TextField="@nameof(TestItem.Name)"
ValueField="@nameof(TestItem.Id)" />
<TelerikButton OnClick="@(() => throw new Exception("Exception from button"))">Click to test ErrorBoundary</TelerikButton>
@code {
private int? SelectedDropDownValue;
private List<TestItem> DropDownData = new()
{
new() { Id = 1, Name = "Select me to throw exception" },
};
private async Task OnDropDownValueChanged(int newValue)
{
throw new InvalidOperationException("This exception should be caught by the ErrorBoundary!");
}
public class TestItem
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
}
The exception thrown in the OnDropDownValueChanged event handler is not caught by ErrorBoundary.
The exception thrown in the OnDropDownValueChanged event handler is caught by ErrorBoundary.
All
No response
Hi !
I tried using the combobox but, since my datasource is too big and I need grouping, therefore virtualization is not possible, I need to do the filtering on the server side, using the OnRead method to fetch my data based on what the user has entered in the input field. The problem is that the client side filtering is always active and I can't reproduce the same type of filtering I do server side on the client side and I lose some results. I think it would be really nice if we could specify to not filter client side or something like that, to give us more control.
Thank you very much !
Hello,
I would like to have the Polar chart available in Blazor.
Radar Chart are nice but the categorical axis are not usable when having directional data to display
Thank you
Thomas
Hi Team,
It might be a better user experience if the user does not have a valid license key to either:
a) make it clear that if the downloader does not have a license assigned the downloaded license key will not be valid
or
b) do not let anyone without a license assigned download the key file (that seems preferable & probably easier to implement)
Thank you!
In our application we use some large datasets and present them in a TelerikGrid. We use WPF + Blazor Hybrid and noticed, that in some cases the memory usage of the Web View process grows up to some gigabytes.
Here a screenshot of the task manager with a lot of RAM usage for the web view.
Here a screenshot of the detached DOM elements after a two navigations. The container divs are not garbage collected.
I tracked down the issue to come from the TelerikGrid, because when I remove it from the pages, everything runs fine. I also removed all GridColumns and the issue is still present. In the developer tools I noticed that one of the parent div elements remains in memory every time I navigate back and forth.
I also created a blank Blazor WebAssembly Standalone application and added a simple instance of the grid. Here, the issue is also present. I attach the one blazor page that causes the issue.
I've tested all major versions from 5.1 upwards, every version is affected.
I have a grid with inline-edit mode where the items have data annotations validation enabled.
When I click the grid command button "add", and then without typing in anything submit in some way, the validation jumps in as it should.
However, if I - without providing more input and still in the same item's edit mode - just click the "add" button again and then submit the item again, the incomplete item is submitted without any further validation.
This is fatal for my purpose, and I can even reproduce the issue here on the Telerik website's example repl: Blazor Grid Editing Inline Editing - Telerik UI for Blazor (after turning off the option "Confirm Cancel Commands").
I would very much appreciate any guidance on how to circumvent that bug while it ist being worked on, since I couldn't yet find a way how to do it.
(As implied above, the confirmation prompt does prevent the bug, however I don't want to use a prompt if possible.)
Here's a list with some cases concerning this bug:
- tap add, submit => validation
- tap add, tap add, submit => submitted!
- tap add, submit (=> validation), tap add, submit => submitted!
Greetings to the team!
In the Appearance section of the ContextMenu component documentation on the Progress Design System Kit website (https://www.telerik.com/design-system/docs/components/contextmenu/#size), it is stated that:
The ContextMenu provides the size configuration option that enables you to control how big or small the rendered submenu items in the popup will be.
I’ve observed that this functionality is implemented in at least one library—Kendo UI for Angular (https://www.telerik.com/kendo-angular-ui/components/menus/contextmenu/appearance#size).
Is there a plan to introduce support for the Size parameter in the ContextMenu component of the Telerik UI for Blazor library?
The problematic behavior can be reproduced in the following case:
In this scenario, when the user filters, the popup position remains unchanged but its height is reduced to fit the filter results. Thus, the popup looks detached from the main element. The behavior is not the same when the popup is rendered below the main element as in this case its top part sticks to the main element.
Reproduction: https://blazorrepl.telerik.com/mpEHGzOt25F1Znxi57.
===
ADMIN EDIT
===
As a workaround for the time being, you can ensure that the popup has a fixed height, so it does not collapse when there are less items to show. You may keep the default popup height or set your desired one through the settings tag. For example: https://blazorrepl.telerik.com/mpYdcfaN38JvIHgP41.Add support for DateOnly and TimeOnly properties to the ToODataString() method in the DataSourceExtensions namespace.
===
TELERIK NOTE
@Steven I forked this thread from the already complete feature request Provide support for DateOnly and TimeOnly structs for the respective pickers
Currently DateOnly and TimeOnly types are not supported by the ToODataString() method. As a result, they are serialized with the default ToString() method. Please use DateTime instead or implement a custom serialization method. Downloading our source code and using the built-in method as a base is also an option.
===
ORIGINAL POST
Would you have any updates regarding this support for early 2024?