Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release/v1.6.0 #97

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
51e3609
ScrollHandler added more Smooth scroll support.
majorimi Sep 8, 2021
3f45335
Added more smooth scroll, bugfixes and demo updates.
majorimi Sep 8, 2021
1dd2573
Permalink service updates added event for perma value detection and u…
majorimi Sep 8, 2021
7ff462f
Implemented INavigationHistoryService
majorimi Sep 8, 2021
0325ae6
Implemented TabActivation with permalinks
majorimi Sep 10, 2021
c90bad7
Merge pull request #96 from majorimi/dev/parmalinkscroll_and_tabs_enh…
majorimi Sep 10, 2021
c4645ce
Updated CollapsePanel animation when heigh is Auto CSS cannot calcula…
majorimi Oct 28, 2021
03a75e2
Updated Collapse docs.
majorimi Oct 28, 2021
550d552
Fixed unit tests and XML commits.
majorimi Oct 28, 2021
09b34f1
Fixed Server Blazor app Null reference.
majorimi Oct 28, 2021
e480b08
Merge pull request #100 from majorimi/fix/collapse-animation
majorimi Oct 29, 2021
a22ddae
Comment out double events
majorimi Dec 1, 2021
223ce03
Fix double event issue added more unit tests.
majorimi Dec 2, 2021
d93d0fb
Merge pull request #106 from majorimi/fix/typeahead-events
majorimi Dec 2, 2021
b9ac1be
Merge branch 'master' into release/v1.6.0
majorimi Dec 2, 2021
5d3eb52
Added some Get methods to IGoogleMapService. Updated demo and docs.
majorimi Dec 19, 2021
aae48ba
Merge pull request #108 from majorimi/dev/google-maps-methods
majorimi Dec 20, 2021
275eeb7
Implemented GoogleMap restriction boundaries in Init
majorimi Dec 20, 2021
a75b7b0
Added comments for maps restrictions.
majorimi Jan 1, 2022
22b31d4
Merge pull request #110 from majorimi/dev/google-map-restriction
majorimi Jan 1, 2022
41bc095
Added GoogleMapPolylineOptions
majorimi Jan 22, 2022
47ce2dd
Code subfoldering
majorimi Jan 23, 2022
6d062f7
AddPolyline feature
majorimi Feb 17, 2022
2101978
Typeahead exposed OnFocus event.
majorimi May 30, 2022
8115276
Update Typeahead.md
majorimi May 30, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/docs/Collapse.md
Original file line number Diff line number Diff line change
@@ -42,10 +42,13 @@ Sets the `style` of the `background-color` when tab is not the Active tab. Use H
- **`HoverColor`: `string { get; set; }` (default: "WhiteSmoke") - Required** <br />
Sets the `style` of the `background-color` when button is hovered over with mouse. Use HTML specified: **Color Names**, **RGB**, **HEX** or with **HSL** values.
- **`ContentHeight`: `int { get; set; }` (default: 200) - Required** <br />
Sets the height of Content panel in `px`. 0 is auto.
Sets the height (in reality sets max-height because of CSS transition issues) of Content panel in `px`. 0 is auto.
- **`MaxAllowedContentHeight`: `int { get; set; }` (default: 200) - Required** <br />
Sets the max-height if Content panel `ContentHeight` is set to 0 (auto).
- **`Animate`: `bool { get; set; }` (default: true)** <br />
Determines to apply CSS animation and transition on Collapse state changes or not.
**Note: in case of `auto` height some animation won't work.**
**Note: in case of Content panel `ContentHeight` is set to 0 (auto), then use `MaxAllowedContentHeight` to set max-height CSS property which will be animated.
Also important based on max-height value transition speed for expand/collapse might differ!.**
- **`Disabled`: `bool { get; set; }` (default: false)** <br />
Determines whether the rendered HTML `<input>` element should be disabled or not.
- **`InnerElementReference`: `ElementReference { get; }`** <br />
68 changes: 52 additions & 16 deletions .github/docs/JsInterop.md
Original file line number Diff line number Diff line change
@@ -15,7 +15,6 @@ For code examples [see usage](https://github.com/majorimi/blazor-components/blob
You can try it out by using the [demo app](https://blazorextensions.z6.web.core.windows.net/jsinterop).

# Features

- **Click JS**:
- `ClickBoundariesElement` is a component which wraps the given content to a DIV and subscribes to all click events: `OnOutsideClick`, `OnInsideClick`.
- Also an **injectable `IClickBoundariesHandler` service** for callback event handlers.
@@ -31,8 +30,9 @@ You can try it out by using the [demo app](https://blazorextensions.z6.web.core.
- **Language JS**: is an **injectable `ILanguageService` service** for detect the browser language preference.
- **Browser Date JS**: is an **injectable `IBrowserDateService` service** is a simple JS call to `new Date();` to retrieve client machine date and time.
- **Browser Theme JS**: is an **injectable `IBrowserThemeService` service** to handle Browser color scheme queries and changes.
- **Geo JS**: is an **injectable `IGeolocationService` service** for detect the device Geolocation (GPS position, speed, heading, etc.).
- **Geo JS**: is an **injectable `IGeolocationService` service** for detect the device [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API) (GPS position, speed, heading, etc.).
- **Head JS**: is an **injectable `IHtmlHeadService` service** for accessing and setting HTML document `Head tags`.
- **Browser History JS**: is an **injectable `INavigationHistoryService` service** to access [HTML History API](https://developer.mozilla.org/en-US/docs/Web/API/History) functionality.

## Click JS (See: [demo app](https://blazorextensions.z6.web.core.windows.net/jsinterop#click-js))
**NOTE: Blazor supports `@onclick` event which is equivalent with `OnInsideClick`.
@@ -169,14 +169,13 @@ Exposes a Blazor `ElementReference` of the wrapped around HTML element. It can b
- **`DisposeAsync()`: `ValueTask IAsyncDisposable()` interface** <br />
Component implements `IAsyncDisposable` interface Blazor framework components also can `@implements IAsyncDisposable` where the injected service should be Disposed.


### `IScrollHandler` Functions
- **`ScrollToElementAsync`**: **`Task ScrollToElementAsync(ElementReference elementReference)`**<br />
Scrolls the given element into the page view area.
- **`ScrollToElementByIdAsync`**: **`Task ScrollToElementByIdAsync(string id)`**<br />
Finds element by Id and scrolls the given element into the page view area.
- **`ScrollToElementByNameAsync`**: **`Task ScrollToElementByNameAsync(string name)`**<br />
Finds element by name and scrolls the given element into the page view area.
- **`ScrollToElementAsync`**: **`Task ScrollToElementAsync(ElementReference elementReference, bool smooth)`**<br />
Scrolls the given element into the page view area. **Note: smooth scroll on element level might not supported by all browsers.**
- **`ScrollToElementByIdAsync`**: **`Task ScrollToElementByIdAsync(string id, bool smooth)`**<br />
Finds element by Id and scrolls the given element into the page view area. **Note: smooth scroll on element level might not supported by all browsers.**
- **`ScrollToElementByNameAsync`**: **`Task ScrollToElementByNameAsync(string name, bool smooth)`**<br />
Finds element by name and scrolls the given element into the page view area. **Note: smooth scroll on element level might not supported by all browsers.**
- **`ScrollToPageEndAsync`**: **`Task ScrollToPageEndAsync(bool smooth)`**<br />
Scrolls to end of the page (X bottom).
- **`ScrollToPageTopAsync`**: **`Task ScrollToPageTopAsync(bool smooth)`**<br />
@@ -197,18 +196,34 @@ Removes event listener for 'scroll' HTML event for the whole document/window by
Implements `IAsyncDisposable` interface the injected service should be Disposed.

### `ElementReference` extensions
- **`ScrollToElementAsync`**: **`Task ScrollToElementAsync(this ElementReference elementReference)`**<br />
- **`ScrollToEndAsync`**: **`Task ScrollToEndAsync(this ElementReference elementReference)`**<br />
- **`ScrollToTopAsync`**: **`Task ScrollToTopAsync(this ElementReference elementReference)`**<br />
- **`ScrollToXAsync`**: **`Task ScrollToXAsync(this ElementReference elementReference, double xPos)`**<br />
- **`ScrollToYAsync`**: **`Task ScrollToYAsync(this ElementReference elementReference, double yPos)`**<br />
- **`GetScrollPositionAsync`**: **`Task<double> GetScrollPositionAsync(this ElementReference elementReference)`**<br />
- **`ScrollToElementAsync`**: **`Task ScrollToElementAsync(this ElementReference elementReference, bool smooth = false)`**<br />
Scrolls HTML page to given element. **Note: smooth scroll on element level might not supported by all browsers.**
- **`ScrollToEndAsync`**: **`Task ScrollToEndAsync(this ElementReference elementReference, bool smooth = false)`**<br />
Scrolls inside the given element to the bottom (end). **Note: smooth scroll on element level might not supported by all browsers.**
- **`ScrollToTopAsync`**: **`Task ScrollToTopAsync(this ElementReference elementReference, bool smooth = false)`**<br />
Scrolls inside the given element to the beginning (top). **Note: smooth scroll on element level might not supported by all browsers.**
- **`ScrollToXAsync`**: **`Task ScrollToXAsync(this ElementReference elementReference, double xPos, bool smooth = false)`**<br />
Scrolls inside the given element to the given X position. **Note: smooth scroll on element level might not supported by all browsers.**
- **`ScrollToYAsync`**: **`Task ScrollToYAsync(this ElementReference elementReference, double yPos, bool smooth = false)`**<br />
Scrolls inside the given element to the given Y position. **Note: smooth scroll on element level might not supported by all browsers.**
- **`ScrollToAsync`**: **`Task ScrollToYAsync(this ElementReference elementReference, double xPos, double yPos, bool smooth = false)`**<br />
Scrolls inside the given element to the given X and Y positions. **Note: smooth scroll on element level might not supported by all browsers.**
- **`GetScrollXPositionAsync`**: **`Task<double> GetScrollXPositionAsync(this ElementReference elementReference)`**<br />
Returns given element scroll X (left) position.
- **`GetScrollYPositionAsync`**: **`Task<double> GetScrollYPositionAsync(this ElementReference elementReference)`**<br />
Returns given element scroll Y (top) position.
- **`IsElementHiddenAsync`**: **`Task<bool> IsElementHiddenAsync(this ElementReference elementReference)`**<br />
Returns given element is visible on HTML document or not.
- **`IsElementHiddenBelowAsync`**: **`Task<bool> IsElementHiddenBelowAsync(this ElementReference elementReference)`**<br />
Returns given element is below of the view port.
- **`IsElementHiddenAboveAsync`**: **`Task<bool> IsElementHiddenAboveAsync(this ElementReference elementReference)`**<br />
Returns given element is above of the view port.
- **`ScrollToElementInParentAsync`**: **`Task ScrollToElementInParentAsync(this ElementReference parent, ElementReference innerElement)`**<br />
Scrolls inside the given parent element to the given inner element.
- **`ScrollInParentByIdAsync`**: **`Task ScrollInParentByIdAsync(this ElementReference parent, string id)`**<br />
Scrolls inside the given parent element to the given inner element by Id.
- **`ScrollInParentByClassAsync`**: **`Task ScrollInParentByClassAsync(this ElementReference parent, string className)`**<br />
Scrolls inside the given parent element to the given first found inner element by class name.

## Resize JS (See: [demo app](https://blazorextensions.z6.web.core.windows.net/jsinterop#resize-js))
**Resize JS** is an **injectable `IResizeHandler` service** for Window (global) and HTML Elements resize event callback handlers.
@@ -275,7 +290,6 @@ Removes event listener for `prefers-color-scheme` HTML event for the Browser.
- **`DisposeAsync`: `ValueTask IAsyncDisposable()` interface** <br />
Implements `IAsyncDisposable` interface the injected service should be Disposed.


## Geolocation JS (See: [demo app](https://blazorextensions.z6.web.core.windows.net/jsinterop#geo-js))
**Geolocation JS** is an injectable `IGeolocationService` service for **detect the device Geolocation (GPS position, speed, heading, etc.)**.
It is using the Geolocation API which allows users to provide their location to web applications if they desire.
@@ -313,6 +327,26 @@ If you have multiple fav icon tags set in the Head first call `GetHtmlFavIconsAs
- **`DisposeAsync`: `ValueTask IAsyncDisposable()` interface** <br />
Implements `IAsyncDisposable` interface the injected service should be Disposed.

## Browser History JS (See: [demo app](https://blazorextensions.z6.web.core.windows.net/jsinterop#history-js))
**Browser History JS** is an injectable `INavigationHistoryService` service** to access HTML History API functionality.
It is useful when don't want to rely on Blazor `NavigationManager` which does not have access to full History list and when it navigates trigger a page load/update.

### Functions
- **`GetLengthAsync`**: **`ValueTask<int> GetLengthAsync()`** <br />
Returns an Integer representing the number of elements in the session history, including the currently loaded page.
- **`GetScrollRestorationAsync`**: **`ValueTask<string> GetScrollRestorationAsync()`** <br />
Allows web applications to explicitly set default scroll restoration behavior on history navigation. This property can be either `auto` or `manual`.
- **`BackAsync`**: **`ValueTask BackAsync()`** <br />
This asynchronous method goes to the previous page in session history, the same action as when the user clicks the browser's Back button. Equivalent to history.go(-1).
- **`ForwardAsync`**: **`ValueTask ForwardAsync()`** <br />
This asynchronous method goes to the next page in session history, the same action as when the user clicks the browser's Forward button; this is equivalent to history.go(1).
- **`GoAsync`**: **`ValueTask GoAsync(int delta)`** <br />
Asynchronously loads a page from the session history, identified by its relative location to the current page, for example -1 for the previous page or 1 for the next page.
- **`ReplaceStateAsync`**: **`ValueTask ReplaceStateAsync(ExpandoObject? state, string title, string url)`** <br />
Updates the most recent entry on the history stack to have the specified data, title, and, if provided, URL.
- **`PushStateAsync`**: **`ValueTask PushStateAsync(ExpandoObject? state, string title, string url)`** <br />
Pushes the given data onto the session history stack with the specified title (and, if provided, URL).


# Configuration

@@ -351,6 +385,8 @@ Add using statement to your Blazor <component/page>.razor file. Or globally refe
@using Majorsoft.Blazor.Components.Common.JsInterop.BrowserDate
@*Only if you want to use Browser ColorTheme*@
@using Majorsoft.Blazor.Components.Common.JsInterop.BrowserColorTheme
@*Only if you want to use Browser History*@
@using Majorsoft.Blazor.Components.Common.JsInterop.History
```


13 changes: 10 additions & 3 deletions .github/docs/Maps.md
Original file line number Diff line number Diff line change
@@ -19,16 +19,22 @@ It is available in the service provider (Google, Microsoft, etc.) developer site
**NOTE: None of the Majorsoft Maps component tracking or exposing you _Token_ or _API Key_!
Injecting and protecting this _Token_ or _API Key_ in your Blazor application is YOUR responsibility!**

# Components
# Components and Services


#### Google:
- **`GoogleStaticMap`**: component is wrapping **Google Static Maps services** into Blazor components.
- **`GoogleMap`**: component is wrapping **Google JavaScript Maps services** into Blazor components.
- **`BindMap`**: _Planned in release v1.4.0_
- **`IGoogleMapService`**: Injectable service to handle Google JavaScript Maps functionalities. Available on the instance of `GoogleMap` object ref as well.

#### Bing:
- **`BindMap`**: _Planned in release v1.6.0_

Maps using `IGeolocationService` (see "Dependences") to center current position.
It can be omitted and injected separately to your components as well to get or track device location.
To see how it works please check **Geo JS** [documentation](https://github.com/majorimi/blazor-components/blob/master/.github/docs/JsInterop.md#geolocation-js-see-demo-app) and [demo](https://blazorextensions.z6.web.core.windows.net/jsinterop#geo-js).


## `GoogleStaticMap` component (See: [demo app](https://blazorextensions.z6.web.core.windows.net/maps#google-static-maps))

:warning: **To use Google Maps Platform, you must have a billing account. The billing account is used to track costs associated with your projects.**
@@ -93,7 +99,6 @@ Once operation has finished successfully `OnLocationDetected` event will be fire
- **`DisposeAsync()`: `Task DisposeAsync()`** <br />
Component implements `IAsyncDisposable` interface Blazor framework will call it when parent removed from render tree.


## `GoogleMap` component (See: [demo app](https://blazorextensions.z6.web.core.windows.net/maps#google-js-maps))

:warning: **To use Google Maps Platform, you must have a billing account. The billing account is used to track costs associated with your projects.**
@@ -111,6 +116,8 @@ You can learn about Google JavaScript Maps features and usage [here](https://dev
Exposes a Blazor `ElementReference` of the wrapped around HTML element. It can be used e.g. for JS interop, etc.
- **`MapId`: `string { get; }`** <br />
Map HTML container Id. It can be used when multiple Maps added to one page.
- **`GoogleMapService`: `string { get; }`** <br />
Exposes `IGeolocationService` which is handling JsInterop. This instance can be used for access more GoogleMap features.
- **`Width`: `int { get; set; }` (default: 400)** <br />
Maps image Width in px.
- **`Height`: `int { get; set; }` (default: 300)** <br />
20 changes: 18 additions & 2 deletions .github/docs/PermaLink.md
Original file line number Diff line number Diff line change
@@ -32,9 +32,17 @@ For more details see Usage section.
### Functions
- **`WatchPermaLinks()`**: **`void WatchPermaLinks()`** <br />
Starts a navigation watcher which will check for Permalinks in the URLs.
- **`ChangePermalink()`**: **`void ChangePermalink(string? newPermalink, bool doNotNavigate)`** <br />
Modify the current URL with given new peralink value and trigger navigation or just update browser History.
- **`CheckPermalink()`**: **`string? CheckPermalink(bool triggerEvent = false)`** <br />
Checks the current URL for permalink again and re-triggers `PermalinkDetected` event if requested.
- **`Dispose()`: `@implements IDisposable` interface** <br />
Component implements `IDisposable` interface Blazor framework will call it when parent removed from render tree.

### Events
- **`PermalinkDetected`: `event EventHandler<PermalinkDetectedEventArgs>** <br />
Event handler for parmalinks detected on navigation.

## `PermaLinkElement` component

### Properties
@@ -68,9 +76,17 @@ Callback function called when Permalink icon clicked and **`PermaLinkIconActions
## PermaLinkBlazorServerInitializer
Available from v1.4.0 It is convenient wrapper component to initialize navigation watcher in your Blazor Server App `MainLayout.razor` page. **Only one Initializer component allowed per Application.**

### Properties
- **`SmootScroll`: `bool { get; set; }` (default: false)** <br />
Scroll should be jump or smoothly scroll. **Note: smooth scroll on element level might not supported by all browsers.**

## PermalinkBlazorWasmInitializer
Available from v1.4.0 It is convenient wrapper component to initialize navigation watcher in your Blazor WebAssembly App `MainLayout.razor` page. **Only one Initializer component allowed per Application.**

### Properties
- **`SmootScroll`: `bool { get; set; }` (default: false)** <br />
Scroll should be jump or smoothly scroll. **Note: smooth scroll on element level might not supported by all browsers.**

# Configuration

## Installation
@@ -141,7 +157,7 @@ Also instance should be disposed.
```
@*Permalink initialize*@
@using Majorsoft.Blazor.Components.PermaLink
<PermalinkBlazorWasmInitializer />
<PermalinkBlazorWasmInitializer SmoothScroll="false" />
```

#### Server hosted projects
@@ -208,7 +224,7 @@ It has to be instantiated manually by using the following code. Also instance sh
```
@*Permalink initialize*@
@using Majorsoft.Blazor.Components.PermaLink
<PermaLinkBlazorServerInitializer />
<PermaLinkBlazorServerInitializer SmoothScroll="false" />
```

#### Creating permalink (#) navigation points inside a Blazor page
45 changes: 41 additions & 4 deletions .github/docs/Tabs.md
Original file line number Diff line number Diff line change
@@ -37,6 +37,9 @@ Sets all `TabItem` elements height in `px`.
Sets all `TabItem` elements element width in `px`.
- **`Disabled`: `bool { get; set; }` (default: false)** <br />
Determines whether all the rendered HTML elements should be disabled or not.
- **`AllowTabActivationByPermalink`: `bool { get; set; }` (default: true)** <br />
Enables or disables `TabItem` activation with URL Permalink fragment.
**NOTE: in order to make TabActivation work `Majorsoft.Blazor.Components.PermaLink` component is used and it MUST [set up correctly](https://github.com/majorimi/blazor-components/blob/master/.github/docs/PermaLink.md#configuration)!**
- **`Animate`: `bool { get; set; }` (default: true)** <br />
Determines to apply CSS animation and transion on Tab changes or not.
- **`TabPositon`: `TabPositons { get; set; }` (default: TabPositons.Left)** <br />
@@ -67,6 +70,9 @@ Required HTML content to show content of current TabItem.
Determines whether the current rendered TabItem should be disabled or not.
- **`Hidden`: `bool { get; set; }` (default: false)** <br />
Determines whether the current rendered TabItem should be hidden or not.
- **`Permalink`: `string { get; set; }` (default: "")** <br />
Permalink value to append to the URL and activate the `TabItem` based on matching value.
**NOTE: in order to make TabActivation work `Majorsoft.Blazor.Components.PermaLink` component is used and it MUST [set up correctly](https://github.com/majorimi/blazor-components/blob/master/.github/docs/PermaLink.md#configuration)!**

**Arbitrary HTML attributes e.g.: `tabindex="1"` will be passed to the corresponding rendered HTML element `<input>`**.

@@ -88,11 +94,42 @@ Add using statement to your Blazor `<component/page>.razor` file. Or globally re
@using Majorsoft.Blazor.Components.Tabs
```

### Dependences
**Majorsoft.Blazor.Components.Tabs** package "partially" depends on other Majorsoft Nuget packages:
- [Majorsoft.Blazor.Components.Common.JsInterop](https://www.nuget.org/packages/Majorsoft.Blazor.Components.Common.JsInterop)
which handles JS Interop for many features e.g. scrolling, etc.
- [Majorsoft.Blazor.Components.Common.PermaLink](https://www.nuget.org/packages/Majorsoft.Blazor.Components.PermaLink)
which track navigations (URL changes) and identify permalink elements.

**NOTE: only TabItem activation feature depend on Permalink. If you don't want to use that feature just leave `Permalink` parameters empty and do not setup PermalinkWatcher.
Also later this feature can be disabled by `AllowTabActivationByPermalink = false`.**

### `TabsPanel` and `TabItem` usage

Following code example shows how to use **`TabsPanel`** with **`TabItem`** component in your Blazor App.

**NOTE: to use TabActivation feature `Permalink="Tab1"` must be set and Permalink services must be [configured correctly](https://github.com/majorimi/blazor-components/blob/master/.github/docs/PermaLink.md#configuration)!**

```
@*Simple tab usage*@
<TabsPanel>
<TabItems>
<TabItem>
<Header>Tab1</Header>
<Content>Tab1</Content>
</TabItem>
<TabItem>
<Header>Tab2</Header>
<Content>Tab2</Content>
</TabItem>
<TabItem>
<Header>Tab3</Header>
<Content>Tab3</Content>
</TabItem>
</TabItems>
</TabsPanel>
@*Advanced tab usage*@
<TabsPanel @ref="_tabs"
ActiveColor="@_activeColor"
InactiveColor="@_inactiveColor"
@@ -105,26 +142,26 @@ Following code example shows how to use **`TabsPanel`** with **`TabItem`** comp
Animate="@_isAnimated"
OnTabChanged="OnTabChanged">
<TabItems>
<TabItem id="tab1" @ref="_tab1">
<TabItem id="tab1" @ref="_tab1" Disabled="false" Permalink="Tab1" Hidden="false">
<Header><strong>Tab 1</strong></Header>
<Content>
<h1>The first tab</h1>
</Content>
</TabItem>
<TabItem @ref="_tab2">
<TabItem @ref="_tab2" Disabled="false" Permalink="Tab2" Hidden="false">
<Header><i>Tab 2</i></Header>
<Content>
<h1>The second tab</h1>
</Content>
</TabItem>
<TabItem id="tab3" @ref="_tab3" Disabled="@_isTabDisabled" Hidden="@_isTabHidden">
<TabItem id="tab3" @ref="_tab3" Disabled="@_isTabDisabled" Permalink="Tab3" Hidden="@_isTabHidden">
<Header><u>Can disable</u></Header>
<Content>
<h1>This tab can be disabled</h1>
<p>And also any <code>TabItem</code> can be disabled by using <code>Disabled</code> property.</p>
</Content>
</TabItem>
<TabItem id="tab4" @ref="_tab4">
<TabItem id="tab4" @ref="_tab4" Disabled="false" Permalink="Tab4" Hidden="false">
<Header>Header icon <i class="fa fa-home"></i></Header>
<Content>
<h1>Tab with icon in header</h1>
2 changes: 2 additions & 0 deletions .github/docs/Typeahead.md
Original file line number Diff line number Diff line change
@@ -104,6 +104,8 @@ Optional HTML content to show when **Search** is in progress.
Callback function called when typeahead dropdown panel opened.
- **`OnDropdownClose`: `EventCallback` delegate** <br />
Callback function called when typeahead dropdown panel opened.
- **`OnFocus`: `EventCallback<FocusEventArgs>` delegate** <br />
Callback function called when typeahead textbox got focus.

### Functions
- **`DisposeAsync()`: `@implements IAsyncDisposable` interface** <br />
42 changes: 21 additions & 21 deletions src/Majorsoft.Blazor.Components.Collapse.Tests/CollapsePanelTest.cs
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ public void CollapsePanel_should_rendered_correctly_html_attributes()
var id = div.FirstElementChild.GetAttribute("id");
div.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" title=""text"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(211,211,211);"" ></div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; height: 200px;"" ></div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; max-height: 200px;"" ></div>
</div>");
}

@@ -43,7 +43,7 @@ public void CollapsePanel_should_rendered_correctly_Disabled()
var id = div.GetAttribute("id");
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" disabled="""" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(211,211,211);"" ></div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; height: 200px;"" ></div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; max-height: 200px;"" ></div>
</div>");
}

@@ -61,13 +61,13 @@ public void CollapsePanel_should_rendered_correctly_CommonHeader()
var id = div.GetAttribute("id");
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(211,211,211);"">Common header</div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; height: 200px;"" ></div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; max-height: 200px;"" ></div>
</div>");

div.Click();
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(211,211,211);"">Common header</div>
<div class=""collapseContent animate"" style=""opacity: 0; overflow: hidden; height: 0px;"" ></div>
<div class=""collapseContent animate"" style=""opacity: 0; overflow: hidden; max-height: 0px;"" ></div>
</div>");
}

@@ -83,13 +83,13 @@ public void CollapsePanel_should_rendered_correctly_ExpandedHeaderContent()
var id = div.GetAttribute("id");
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(211,211,211);"">Expanded header</div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; height: 200px;"" ></div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; max-height: 200px;"" ></div>
</div>");

div.Click();
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(211,211,211);""></div>
<div class=""collapseContent animate"" style=""opacity: 0; overflow: hidden; height: 0px;"" ></div>
<div class=""collapseContent animate"" style=""opacity: 0; overflow: hidden; max-height: 0px;"" ></div>
</div>");
}

@@ -105,13 +105,13 @@ public void CollapsePanel_should_rendered_correctly_CollapsedHeaderContent()
var id = div.GetAttribute("id");
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(211,211,211);""></div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; height: 200px;"" ></div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; max-height: 200px;"" ></div>
</div>");

div.Click();
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(211,211,211);"">Collapsed header</div>
<div class=""collapseContent animate"" style=""opacity: 0; overflow: hidden; height: 0px;"" ></div>
<div class=""collapseContent animate"" style=""opacity: 0; overflow: hidden; max-height: 0px;"" ></div>
</div>");
}

@@ -128,13 +128,13 @@ public void CollapsePanel_should_rendered_correctly_ExpandedColor()
var id = div.GetAttribute("id");
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(255,0,0);"">Expanded header</div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; height: 200px;"" ></div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; max-height: 200px;"" ></div>
</div>");

div.Click();
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(211,211,211);""></div>
<div class=""collapseContent animate"" style=""opacity: 0; overflow: hidden; height: 0px;"" ></div>
<div class=""collapseContent animate"" style=""opacity: 0; overflow: hidden; max-height: 0px;"" ></div>
</div>");
}

@@ -151,13 +151,13 @@ public void CollapsePanel_should_rendered_correctly_CollapsedColor()
var id = div.GetAttribute("id");
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(211,211,211);""></div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; height: 200px;"" ></div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; max-height: 200px;"" ></div>
</div>");

div.Click();
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(255,0,0);"">Collapsed header</div>
<div class=""collapseContent animate"" style=""opacity: 0; overflow: hidden; height: 0px;"" ></div>
<div class=""collapseContent animate"" style=""opacity: 0; overflow: hidden; max-height: 0px;"" ></div>
</div>");
}

@@ -173,27 +173,27 @@ public async Task CollapsePanel_should_rendered_correctly_HoverColor()
var id = div.GetAttribute("id");
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(211,211,211);""></div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; height: 200px;"" ></div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; max-height: 200px;"" ></div>
</div>");

await div.TriggerEventAsync("onmouseenter", new MouseEventArgs());
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(255,0,0);""></div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; height: 200px;"" ></div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; max-height: 200px;"" ></div>
</div>");
await div.TriggerEventAsync("onmouseleave", new MouseEventArgs());


div.Click();
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(211,211,211);""></div>
<div class=""collapseContent animate"" style=""opacity: 0; overflow: hidden; height: 0px;"" ></div>
<div class=""collapseContent animate"" style=""opacity: 0; overflow: hidden; max-height: 0px;"" ></div>
</div>");

await div.TriggerEventAsync("onmouseenter", new MouseEventArgs());
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(255,0,0);""></div>
<div class=""collapseContent animate"" style=""opacity: 0; overflow: hidden; height: 0px;"" ></div>
<div class=""collapseContent animate"" style=""opacity: 0; overflow: hidden; max-height: 0px;"" ></div>
</div>");
}

@@ -211,14 +211,14 @@ public void CollapsePanel_should_rendered_correctly_Content()
var id = div.GetAttribute("id");
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(211,211,211);"">Expanded header</div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; height: 200px;"" >Content...</div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; max-height: 200px;"" >Content...</div>
</div>");

rendered.SetParametersAndRender(parameters => parameters
.Add(p => p.Collapsed, true));
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(211,211,211);"">Collapsed header</div>
<div class=""collapseContent animate"" style=""opacity: 0; overflow: hidden; height: 0px;"" >Content...</div>
<div class=""collapseContent animate"" style=""opacity: 0; overflow: hidden; max-height: 0px;"" >Content...</div>
</div>");
}

@@ -234,7 +234,7 @@ public void CollapsePanel_should_rendered_correctly_ContentHeight()
var id = div.GetAttribute("id");
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader animate"" style=""background-color: rgb(211,211,211);"" ></div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; height: 55px;"" ></div>
<div class=""collapseContent animate"" style=""opacity: 1; overflow: hidden; max-height: 55px;"" ></div>
</div>");
}

@@ -250,7 +250,7 @@ public void CollapsePanel_should_rendered_correctly_Animate()
var id = div.GetAttribute("id");
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader"" style=""background-color: rgb(211,211,211);"" ></div>
<div class=""collapseContent"" style=""opacity: 1; overflow: hidden; height: 200px;"" ></div>
<div class=""collapseContent"" style=""opacity: 1; overflow: hidden; max-height: 200px;"" ></div>
</div>");
}

@@ -267,7 +267,7 @@ public void CollapsePanel_should_rendered_correctly_ShowContentOverflow()
var id = div.GetAttribute("id");
rendered.MarkupMatches(@$"<div class=""collapsePanel"" tabindex=""200"" >
<div id=""{id}"" class=""collapseHeader"" style=""background-color: rgb(211,211,211);"" ></div>
<div class=""collapseContent"" style=""opacity: 1; overflow: auto; height: 200px;"" ></div>
<div class=""collapseContent"" style=""opacity: 1; overflow: auto; max-height: 200px;"" ></div>
</div>");
}
}
15 changes: 11 additions & 4 deletions src/Majorsoft.Blazor.Components.Collapse/CollapsePanel.razor
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@
}
</div>
<div class="collapseContent @(Animate ? "animate" : "")"
style="opacity: @(Collapsed ? "0" : "1"); overflow: @(ShowContentOverflow ? "auto" : "hidden"); height: @GetContentHeight()">
style="opacity: @(Collapsed ? "0" : "1"); overflow: @(ShowContentOverflow ? "auto" : "hidden"); max-height: @GetContentHeight()">
@Content
</div>
</div>
@@ -83,10 +83,16 @@
}

/// <summary>
/// Sets the height of Content panel in `px`. 0 is auto.
/// Sets the height (in reality sets max-height because of CSS transition issues) of Content panel in `px`. 0 is auto.
/// </summary>
[Parameter] public int ContentHeight { get; set; } = 200;

/// <summary>
/// Sets the max-height if Content panel <see cref="ContentHeight"/> is set to 0 (auto).
/// https://css-tricks.com/using-css-transitions-auto-dimensions/
/// </summary>
[Parameter] public int MaxAllowedContentHeight { get; set; } = 200;

/// <summary>
/// Determines whether all the rendered HTML elements should be disabled or not.
/// </summary>
@@ -114,7 +120,8 @@

/// <summary>
/// Determines to apply CSS animation and transition on Collapse state changes or not.
/// Note: in case of `auto` height some animation won't work.
/// Note: in case of Content panel <see cref="ContentHeight"/> is set to 0 (auto), then use <see cref="MaxAllowedContentHeight"/> to set max-height CSS property which will be animated.
/// Also important based on max-height value transition speed for expand/collapse might differ!
/// </summary>
[Parameter] public bool Animate { get; set; } = true;

@@ -158,7 +165,7 @@
return "0px;";
}

return ContentHeight <= 0 ? "auto;" : $"{ContentHeight}px;";
return ContentHeight <= 0 ? $"{MaxAllowedContentHeight}px;" : $"{ContentHeight}px;";
}
private string GetStyle()
{
Original file line number Diff line number Diff line change
@@ -22,10 +22,11 @@
}

.collapseContent {
height: auto;
outline: none;
display: block;
}
.collapseContent.animate {
-webkit-transition: opacity 1s, height 0.4s; /* For Safari 3.1 to 6.0 */
transition: opacity 1s, height 0.4s;
-webkit-transition: opacity 1s, max-height 0.8s; /* For Safari 3.1 to 6.0 */
transition: opacity 1s, max-height 0.8s;
}
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@
using Majorsoft.Blazor.Components.Common.JsInterop.GlobalMouseEvents;
using Majorsoft.Blazor.Components.Common.JsInterop.Head;
using Majorsoft.Blazor.Components.Common.JsInterop.Language;
using Majorsoft.Blazor.Components.Common.JsInterop.Navigation;
using Majorsoft.Blazor.Components.Common.JsInterop.Resize;
using Majorsoft.Blazor.Components.Common.JsInterop.Scroll;

@@ -44,6 +45,7 @@ public static IServiceCollection AddJsInteropExtensions(this IServiceCollection
services.AddTransient<IHtmlHeadService, HtmlHeadService>();
services.AddTransient<IBrowserDateService, BrowserDateService>();
services.AddTransient<IBrowserThemeService, BrowserThemeService>();
services.AddTransient<INavigationHistoryService, NavigationHistoryService>();

return services;
}
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@
<RepositoryType>Git</RepositoryType>
<Description>Blazor component that provides useful functionality and event notifications which can be achieved only with JS Interop e.g. scroll, clipboard, focus, resize, language detection, GEO location, etc. Part of Majorsoft Blazor library.</Description>
<PackageReleaseNotes>See Releases here: https://github.com/majorimi/blazor-components/releases</PackageReleaseNotes>
<PackageTags>.Net5 Blazor Js Interop Click Scroll Focus BoundRect</PackageTags>
<PackageTags>.Net5 Blazor Js Interop Click Scroll Resize Focus BoundRect Browser Geolocation Head History ColorTheme</PackageTags>
<PackageLicenseFile>License.txt</PackageLicenseFile>
<Title>Blazor Components JsInterop</Title>
</PropertyGroup>
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Dynamic;
using System.Threading.Tasks;

namespace Majorsoft.Blazor.Components.Common.JsInterop.Navigation
{
/// <summary>
/// Injectable service to handle Browser history JS Interops.
/// https://developer.mozilla.org/en-US/docs/Web/API/History
/// </summary>
public interface INavigationHistoryService
{
/// <summary>
/// Returns an Integer representing the number of elements in the session history, including the currently loaded page. For example, for a page loaded in a new tab this property returns 1.
/// </summary>
/// <returns>ValueTask</returns>
ValueTask<int> GetLengthAsync();

/// <summary>
/// Allows web applications to explicitly set default scroll restoration behavior on history navigation. This property can be either `auto` or `manual`.
/// </summary>
/// <returns>ValueTask</returns>
ValueTask<string> GetScrollRestorationAsync();

/// <summary>
/// This asynchronous method goes to the previous page in session history, the same action as when the user clicks the browser's Back button. Equivalent to history.go(-1).
/// </summary>
/// <returns>ValueTask</returns>
ValueTask BackAsync();

/// <summary>
/// This asynchronous method goes to the next page in session history, the same action as when the user clicks the browser's Forward button; this is equivalent to history.go(1).
/// </summary>
/// <returns>ValueTask</returns>
ValueTask ForwardAsync();

/// <summary>
/// Asynchronously loads a page from the session history, identified by its relative location to the current page, for example -1 for the previous page or 1 for the next page.
/// </summary>
/// <param name="delta">The position in the history to which you want to move, relative to the current page. A negative value moves backwards, a positive value moves forwards.</param>
/// <returns>ValueTask</returns>
ValueTask GoAsync(int delta);

/// <summary>
/// Updates the most recent entry on the history stack to have the specified data, title, and, if provided, URL.
/// </summary>
/// <param name="state">Arbitrary object of the page</param>
/// <param name="title">Page tile</param>
/// <param name="url">New URL to show and add to history</param>
/// <returns>ValueTask</returns>
ValueTask ReplaceStateAsync(ExpandoObject? state, string title, string url);

/// <summary>
/// Pushes the given data onto the session history stack with the specified title (and, if provided, URL).
/// </summary>
/// <param name="state">Arbitrary object of the page</param>
/// <param name="title">Page tile</param>
/// <param name="url">New URL to show and add to history</param>
/// <returns>ValueTask</returns>
ValueTask PushStateAsync(ExpandoObject? state, string title, string url);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Dynamic;
using System.Threading.Tasks;

using Microsoft.JSInterop;

namespace Majorsoft.Blazor.Components.Common.JsInterop.Navigation
{
/// <summary>
/// Implementation of <see cref="INavigationHistoryService"/>
/// </summary>
public class NavigationHistoryService : INavigationHistoryService
{
private readonly IJSRuntime _jSRuntime;

public NavigationHistoryService(IJSRuntime jSRuntime)
{
_jSRuntime = jSRuntime;
}

public async ValueTask<int> GetLengthAsync() => await _jSRuntime.InvokeAsync<int>("eval", "history.length");
public async ValueTask<string> GetScrollRestorationAsync() => await _jSRuntime.InvokeAsync<string>("eval", "history.scrollRestoration");

public async ValueTask BackAsync()
=> await _jSRuntime.InvokeVoidAsync("history.back");

public async ValueTask ForwardAsync()
=> await _jSRuntime.InvokeVoidAsync("history.forward");

public async ValueTask GoAsync(int delta)
=> await _jSRuntime.InvokeVoidAsync("history.go", delta);

public async ValueTask PushStateAsync(ExpandoObject? state, string title, string url)
=> await _jSRuntime.InvokeVoidAsync("history.pushState", state, title, url);

public async ValueTask ReplaceStateAsync(ExpandoObject? state, string title, string url)
=> await _jSRuntime.InvokeVoidAsync("history.replaceState", state, title, url);
}
}
Original file line number Diff line number Diff line change
@@ -12,17 +12,18 @@ namespace Majorsoft.Blazor.Components.Common.JsInterop.Scroll
public static class ElementReferenceScrollExtensions
{
/// <summary>
/// Scrolls HTML page to given element
/// Scrolls HTML page to given element.
/// </summary>
/// <param name="elementReference">Blazor reference to an HTML element</param>
/// <param name="smooth">Scroll should jump or smoothly scroll Note: might not all browsers support it</param>
/// <returns>Async Task</returns>
public static async Task ScrollToElementAsync(this ElementReference elementReference)
public static async Task ScrollToElementAsync(this ElementReference elementReference, bool smooth = false)
{
await using (var module = await elementReference.GetJsObject())
{
if (module is not null)
{
await module.InvokeVoidAsync("scrollToElement", elementReference);
await module.InvokeVoidAsync("scrollToElement", elementReference, smooth);
}
}
}
@@ -31,14 +32,15 @@ public static async Task ScrollToElementAsync(this ElementReference elementRefer
/// Scrolls inside the given element to the bottom (end).
/// </summary>
/// <param name="elementReference">Blazor reference to an HTML element</param>
/// <param name="smooth">Scroll should jump or smoothly scroll Note: might not all browsers support it</param>
/// <returns>Async Task</returns>
public static async Task ScrollToEndAsync(this ElementReference elementReference)
public static async Task ScrollToEndAsync(this ElementReference elementReference, bool smooth = false)
{
await using (var module = await elementReference.GetJsObject())
{
if (module is not null)
{
await module.InvokeVoidAsync("scrollToEnd", elementReference);
await module.InvokeVoidAsync("scrollToEnd", elementReference, smooth);
}
}
}
@@ -47,14 +49,15 @@ public static async Task ScrollToEndAsync(this ElementReference elementReference
/// Scrolls inside the given element to the beginning (top).
/// </summary>
/// <param name="elementReference">Blazor reference to an HTML element</param>
/// <param name="smooth">Scroll should jump or smoothly scroll Note: might not all browsers support it</param>
/// <returns>Async Task</returns>
public static async Task ScrollToTopAsync(this ElementReference elementReference)
public static async Task ScrollToTopAsync(this ElementReference elementReference, bool smooth = false)
{
await using (var module = await elementReference.GetJsObject())
{
if (module is not null)
{
await module.InvokeVoidAsync("scrollToTop", elementReference);
await module.InvokeVoidAsync("scrollToTop", elementReference, smooth);
}
}
}
@@ -64,14 +67,15 @@ public static async Task ScrollToTopAsync(this ElementReference elementReference
/// </summary>
/// <param name="elementReference">Blazor reference to an HTML element</param>
/// <param name="xPos">Scroll X position</param>
/// <param name="smooth">Scroll should jump or smoothly scroll Note: might not all browsers support it</param>
/// <returns>Async Task</returns>
public static async Task ScrollToXAsync(this ElementReference elementReference, double xPos)
public static async Task ScrollToXAsync(this ElementReference elementReference, double xPos, bool smooth = false)
{
await using (var module = await elementReference.GetJsObject())
{
if (module is not null)
{
await module.InvokeVoidAsync("scrollToX", elementReference, xPos);
await module.InvokeVoidAsync("scrollToX", elementReference, xPos, smooth);
}
}
}
@@ -81,20 +85,40 @@ public static async Task ScrollToXAsync(this ElementReference elementReference,
/// </summary>
/// <param name="elementReference">Blazor reference to an HTML element</param>
/// <param name="yPos">Scroll Y position</param>
/// <param name="smooth">Scroll should jump or smoothly scroll Note: might not all browsers support it</param>
/// <returns>Async Task</returns>
public static async Task ScrollToYAsync(this ElementReference elementReference, double yPos)
public static async Task ScrollToYAsync(this ElementReference elementReference, double yPos, bool smooth = false)
{
await using (var module = await elementReference.GetJsObject())
{
if (module is not null)
{
await module.InvokeVoidAsync("scrollToY", elementReference, yPos);
await module.InvokeVoidAsync("scrollToY", elementReference, yPos, smooth);
}
}
}

/// <summary>
/// Returns given element scroll X position.
/// Scrolls inside the given element to the given X and Y positions.
/// </summary>
/// <param name="elementReference">Blazor reference to an HTML element</param>
/// <param name="xPos">Scroll X position</param>
/// <param name="yPos">Scroll Y position</param>
/// <param name="smooth">Scroll should jump or smoothly scroll Note: might not all browsers support it</param>
/// <returns>Async Task</returns>
public static async Task ScrollToAsync(this ElementReference elementReference, double xPos, double yPos, bool smooth = false)
{
await using (var module = await elementReference.GetJsObject())
{
if (module is not null)
{
await module.InvokeVoidAsync("scrollTo", elementReference, xPos, yPos, smooth);
}
}
}

/// <summary>
/// Returns given element scroll X (left) position.
/// </summary>
/// <param name="elementReference">Blazor reference to an HTML element</param>
/// <returns>Async Task with X pos</returns>
@@ -110,6 +134,23 @@ public static async Task<double> GetScrollXPositionAsync(this ElementReference e

return 0;
}
/// <summary>
/// Returns given element scroll Y (top) position.
/// </summary>
/// <param name="elementReference">Blazor reference to an HTML element</param>
/// <returns>Async Task with Y pos</returns>
public static async Task<double> GetScrollYPositionAsync(this ElementReference elementReference)
{
await using (var module = await elementReference.GetJsObject())
{
if (module is not null)
{
return await module.InvokeAsync<double>("getScrollYPosition", elementReference);
}
}

return 0;
}

/// <summary>
/// Returns given element is visible on HTML document or not.
Original file line number Diff line number Diff line change
@@ -14,20 +14,23 @@ public interface IScrollHandler : IAsyncDisposable
/// Scrolls the given element into the page view area.
/// </summary>
/// <param name="elementReference">Blazor reference to an HTML element</param>
/// <param name="smooth">Scroll should jump or smoothly scroll Note: might not all browsers support it</param>
/// <returns>Async Task</returns>
Task ScrollToElementAsync(ElementReference elementReference);
Task ScrollToElementAsync(ElementReference elementReference, bool smooth = false);
/// <summary>
/// Finds element by Id and scrolls the given element into the page view area.
/// </summary>
/// <param name="name">DOM element id</param>
/// <param name="id">DOM element id</param>
/// <param name="smooth">Scroll should jump or smoothly scroll Note: might not all browsers support it</param>
/// <returns>Async Task</returns>
Task ScrollToElementByIdAsync(string id);
Task ScrollToElementByIdAsync(string id, bool smooth = false);
/// <summary>
/// Finds element by name and scrolls the given element into the page view area.
/// </summary>
/// <param name="name">DOM element name</param>
/// <param name="smooth">Scroll should jump or smoothly scroll Note: might not all browsers support it</param>
/// <returns>Async Task</returns>
Task ScrollToElementByNameAsync(string name);
Task ScrollToElementByNameAsync(string name, bool smooth = false);

/// <summary>
/// Scrolls to end of the page (X bottom).
Original file line number Diff line number Diff line change
@@ -54,22 +54,22 @@ public async Task<ScrollResult> GetPageScrollSizeAsync()
return await _scrollJs.InvokeAsync<ScrollResult>("getPageScrollSize");
}

public async Task ScrollToElementAsync(ElementReference elementReference)
public async Task ScrollToElementAsync(ElementReference elementReference, bool smooth)
{
await CheckJsObjectAsync();
await _scrollJs.InvokeVoidAsync("scrollToElement", elementReference);
await _scrollJs.InvokeVoidAsync("scrollToElement", elementReference, smooth);
}

public async Task ScrollToElementByIdAsync(string id)
public async Task ScrollToElementByIdAsync(string id, bool smooth)
{
await CheckJsObjectAsync();
await _scrollJs.InvokeVoidAsync("scrollToElementById", id);
await _scrollJs.InvokeVoidAsync("scrollToElementById", id, smooth);
}

public async Task ScrollToElementByNameAsync(string name)
public async Task ScrollToElementByNameAsync(string name, bool smooth)
{
await CheckJsObjectAsync();
await _scrollJs.InvokeVoidAsync("scrollToElementByName", name);
await _scrollJs.InvokeVoidAsync("scrollToElementByName", name, smooth);
}

public async Task<string> RegisterPageScrollAsync(Func<ScrollEventArgs, Task> scrollCallback)
54 changes: 39 additions & 15 deletions src/Majorsoft.Blazor.Components.Common.JsInterop/wwwroot/scroll.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
/* Extensions */
//Element scrolled to page top
export function scrollToElement(element) {
export function scrollToElement(element, smooth) {
if (element && typeof element.scrollIntoView === "function") {
element.scrollIntoView();
if (smooth) {
element.scrollIntoView({ behavior: 'smooth' });
}
else {
element.scrollIntoView();
}
}
}
export function scrollToElementById(id) {
export function scrollToElementById(id, smooth) {
if (id) {
scrollToElement(document.getElementById(id));
scrollToElement(document.getElementById(id), smooth);
}
}
export function scrollToElementByName(name) {
export function scrollToElementByName(name, smooth) {
if (name) {
let elements = document.getElementsByName(name)
if (elements && elements.length > 0) {
scrollToElement(elements[0]);
scrollToElement(elements[0], smooth);
}
}
}
@@ -75,27 +80,46 @@ export function isElementHiddenAbove(element) {
}

//Scrolling inside an element use it for e.g. Textarea
export function scrollToEnd(element) {
export function scrollToEnd(element, smooth) {
if (element && typeof element.scrollTop !== undef && typeof element.scrollHeight !== undef) {
element.scrollTop = element.scrollHeight;
scrollTo(element, element.scrollLeft, element.scrollHeight, smooth);
}
}
export function scrollToTop(element) {
export function scrollToTop(element, smooth) {
if (element && typeof element.scrollTop !== undef) {
element.scrollTop = 0;
scrollTo(element, element.scrollLeft, 0, smooth);
}
}
export function scrollToX(element, x) {
export function scrollToX(element, x, smooth) {
if (element && typeof element.scrollTop !== undef) {
element.scrollTop = x;
scrollTo(element, x, element.scrollTop, smooth);
}
}
export function scrollToY(element, y) {
export function scrollToY(element, y, smooth) {
if (element && typeof element.scrollLeft !== undef) {
element.scrollLeft = y;
scrollTo(element, element.scrollLeft, y, smooth);
}
}
export function scrollTo(element, x, y, smooth) {
if (element) {
if (smooth) {
element.scroll({
top: y,
left: x,
behavior: 'smooth'});
}
else {
element.scrollTop = y;
element.scrollLeft = x;
}
}
}
export function getScrollXPosition(element) {
if (element && typeof element.scrollLeft !== undef) {
return element.scrollLeft;
}
}
export function getScrollYPosition(element) {
if (element && typeof element.scrollTop !== undef) {
return element.scrollTop;
}
@@ -245,4 +269,4 @@ export function dispose(eventIdArray) {
removeScrollEvent(eventIdArray[i]);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
using System;

namespace Majorsoft.Blazor.Components.Maps.Google
{
/// <summary>
/// PolylineOptions object used to define the properties that can be set on a Polyline.
/// https://developers.google.com/maps/documentation/javascript/reference/polygon#Polyline
/// </summary>
public class GoogleMapPolylineOptions
{
public Guid Id { get; }

/// <summary>
/// Indicates whether this Polyline handles mouse events. Defaults to true.
/// </summary>
public bool Clickable { get; set; }

/// <summary>
/// If set to true, the user can drag this shape over the map. The geodesic property defines the mode of dragging. Defaults to false.
/// </summary>
public bool Draggable { get; set; }

/// <summary>
/// If set to true, the user can edit this shape by dragging the control points shown at the vertices and on each segment. Defaults to false.
/// </summary>
public bool Editable { get; set; }

/// <summary>
/// When true, edges of the polygon are interpreted as geodesic and will follow the curvature of the Earth. When false, edges of the polygon are rendered as
/// straight lines in screen space. Note that the shape of a geodesic polygon may appear to change when dragged, as the dimensions are maintained relative to
/// the surface of the earth. Defaults to false.
/// </summary>
public bool Geodesic { get; set; }

/// <summary>
/// The icons to be rendered along the polyline.
/// </summary>
public GoogleMapIconSequence[] Icons { get; set; }

/// <summary>
/// The ordered sequence of coordinates of the Polyline.
/// </summary>
public GoogleMapLatLng[] Path { get; set; }

/// <summary>
/// The stroke color. All CSS3 colors are supported except for extended named colors.
/// </summary>
public string StrokeColor { get; set; } = "black";

/// <summary>
/// The stroke opacity between 0.0 and 1.0.
/// </summary>
public double StrokeOpacity { get; set; } = 1.0;

/// <summary>
/// The stroke width in pixels.
/// </summary>
public double StrokeWeight { get; set; } = 2;

/// <summary>
/// Whether this polyline is visible on the map. Defaults to true.
/// </summary>
public bool Visible { get; set; } = true;

/// <summary>
/// The zIndex compared to other polys.
/// </summary>
public int ZIndex { get; set; }

public GoogleMapPolylineOptions()
{
Id = Guid.NewGuid();
}
}

/// <summary>
/// Describes how icons are to be rendered on a line.
/// </summary>
public class GoogleMapIconSequence
{
/// <summary>
/// If true, each icon in the sequence has the same fixed rotation regardless of the angle of the edge on which it lies. Defaults to false,
/// in which case each icon in the sequence is rotated to align with its edge.
/// </summary>
public bool FixedRotation { get; set; }

/// <summary>
/// The icon to render on the line.
/// </summary>
public GoogleMapIconSequenceSymbol Icon { get; set; }

/// <summary>
/// The distance from the start of the line at which an icon is to be rendered. This distance may be expressed as a percentage of line's length
/// (e.g. '50%') or in pixels (e.g. '50px'). Defaults to '100%'.
/// </summary>
public string Offset { get; set; }

/// <summary>
/// The distance between consecutive icons on the line. This distance may be expressed as a percentage of the line's length (e.g. '50%') or in pixels
/// (e.g. '50px'). To disable repeating of the icon, specify '0'. Defaults to '0'.
/// </summary>
public string Repeat { get; set; }
}

/// <summary>
/// Describes a symbol, which consists of a vector path with styling. A symbol can be used as the icon of a marker, or placed on a polyline.
/// </summary>
public class GoogleMapIconSequenceSymbol
{
/// <summary>
/// The symbol's path, which is a built-in symbol path, or a custom path expressed using SVG path notation. Required.
/// </summary>
public string Path { get; set; }

/// <summary>
/// The position at which to anchor an image in correspondence to the location of the marker on the map.
/// By default, the anchor is located along the center point of the bottom of the image.
/// </summary>
public Point? Anchor { get; set; }

/// <summary>
/// The symbol's fill color. All CSS3 colors are supported except for extended named colors. For symbol markers, this defaults to 'black'.
/// For symbols on polylines, this defaults to the stroke color of the corresponding polyline.
/// </summary>
public string FillColor { get; set; }

/// <summary>
/// The symbol's fill opacity. Defaults to 0.
/// </summary>
public double FillOpacity { get; set; }

/// <summary>
/// The origin of the label relative to the top-left corner of the icon image, if a label is supplied by the marker.
/// By default, the origin is located in the center point of the image.
/// </summary>
public Point? LabelOrigin { get; set; }

/// <summary>
/// The angle by which to rotate the symbol, expressed clockwise in degrees. Defaults to 0. A symbol in an IconSequence where fixedRotation is false
/// is rotated relative to the angle of the edge on which it lies.
/// </summary>
public double Rotation { get; set; }

/// <summary>
/// The amount by which the symbol is scaled in size. For symbol markers, this defaults to 1; after scaling, the symbol may be of any size. For symbols on a polyline,
/// this defaults to the stroke weight of the polyline; after scaling, the symbol must lie inside a square 22 pixels in size centered at the symbol's anchor.
/// </summary>
public double Scale { get; set; }

/// <summary>
/// The symbol's stroke color. All CSS3 colors are supported except for extended named colors. For symbol markers, this defaults to 'black'.
/// For symbols on a polyline, this defaults to the stroke color of the polyline.
/// </summary>
public string StrokeColor { get; set; }

/// <summary>
/// The symbol's stroke opacity. For symbol markers, this defaults to 1. For symbols on a polyline, this defaults to the stroke opacity of the polyline.
/// </summary>
public double StrokeOpacity { get; set; }

/// <summary>
/// The symbol's stroke weight. Defaults to the scale of the symbol.
/// </summary>
public double StrokeWeight { get; set; }

/// <summary>
/// Default constructor.
/// </summary>
/// <param name="path">The symbol's path</param>
public GoogleMapIconSequenceSymbol(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentException($"'{nameof(path)}' cannot be null or whitespace", nameof(path));
}

Path = path;
}
}
}
15 changes: 14 additions & 1 deletion src/Majorsoft.Blazor.Components.Maps/Google/GoogleMap.razor
Original file line number Diff line number Diff line change
@@ -23,6 +23,11 @@
/// </summary>
public ElementReference InnerElementReference => _jsMap;

/// <summary>
/// Exposes <see cref="IGeolocationService"/> which is handling JsInterop. This instance can be used for access more GoogleMap features.
/// </summary>
public IGoogleMapService GoogleMapService => _mapService;

private bool _mapInitialized = false;
private bool _isDragging = false;
protected override async Task OnInitializedAsync()
@@ -32,6 +37,7 @@
mapContainerId: _mapContainerId,
backgroundColor: BackgroundColor,
controlSize: ControlSize,
restriction: Restriction,
mapInitializedCallback: async (mapId) =>
{
WriteDiag($"Google JavaScript API Map initialzied with DIV Id: '{_mapContainerId}'.");
@@ -131,7 +137,7 @@
},
mapZoomChangedCallback: async (zoom) =>
{
WriteDiag($"Map Zoom level changed to: '{zoom}'.");
WriteDiag($"Map Zoom level callback changed to: '{zoom}'.");
if (_zoomLevel == zoom)
return;

@@ -287,6 +293,11 @@
/// This option can only be set when the map is initialized. Use <see cref="OnInitialized"/> method to set it up.
/// </summary>
[Parameter] public IEnumerable<GoogleMapCustomControl>? CustomControls { get; set; }
/// <summary>
/// Restrictions for Maps by coordinates SW/NE.
/// This option can only be set when the map is initialized. Use <see cref="OnInitialized"/> method to set it up.
/// </summary>
[Parameter] public GoogleMapRestriction? Restriction { get; set; }

private ObservableRangeCollection<GoogleMapMarker>? _markers;
/// <summary>
@@ -325,6 +336,8 @@
if (_mapInitialized && value != _zoomLevel)
{
_zoomLevel = value;

WriteDiag($"Map Zoom level property Set to: '{_zoomLevel}'.");
InvokeAsync(async () => await _mapService.SetZoomAsync(_zoomLevel));
}
}
24 changes: 24 additions & 0 deletions src/Majorsoft.Blazor.Components.Maps/Google/GoogleMapLatLng.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;

namespace Majorsoft.Blazor.Components.Maps.Google
{
/// <summary>
/// A LatLng is a point in geographical coordinates: latitude and longitude.
/// </summary>
public class GoogleMapLatLng
{
/// <summary>
/// Latitude ranges between -90 and 90 degrees, inclusive. Values above or below this range will be clamped to the range [-90, 90].
/// This means that if the value specified is less than -90, it will be set to -90. And if the value is greater than 90, it will be set to 90.
/// </summary>
[JsonPropertyName("lat")]
public double Latitude { get; set; }

/// <summary>
/// Longitude ranges between -180 and 180 degrees, inclusive. Values above or below this range will be wrapped so that they fall within the range.
/// For example, a value of -190 will be converted to 170. A value of 190 will be converted to -170. This reflects the fact that longitudes wrap around the globe.
/// </summary>
[JsonPropertyName("lng")]
public double Longitude { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
namespace Majorsoft.Blazor.Components.Maps.Google
{
/// <summary>
/// A LatLngBounds instance represents a rectangle in geographical coordinates, including one that crosses the 180 degrees longitudinal meridian.
/// </summary>
public class GoogleMapLatLngBounds
{
/// <summary>
/// Default constructor
/// </summary>
public GoogleMapLatLngBounds()
{
}

/// <summary>
/// Initialize object for Map restrictions
/// </summary>
/// <param name="southWest">The south-west corner of this bounds.</param>
/// <param name="northEast">The north-east corner of this bounds.</param>
public GoogleMapLatLngBounds(GoogleMapLatLng southWest, GoogleMapLatLng northEast)
{
SouthWest = southWest;
NorthEast = northEast;
}

/// <summary>
/// Computes the center of this LatLngBounds
/// Equivalent with `getCenter()` method call but it would require JsInterop.
/// </summary>
public GoogleMapLatLng Center { get; set; }

/// <summary>
/// Returns the north-east corner of this bounds.
/// Equivalent with `getNorthEast()` method call but it would require JsInterop.
/// </summary>
public GoogleMapLatLng NorthEast { get; set; }

/// <summary>
/// Returns the south-west corner of this bounds.
/// Equivalent with `getSouthWest()` method call but it would require JsInterop.
/// </summary>
public GoogleMapLatLng SouthWest { get; set; }

/// <summary>
/// Converts the given map bounds to a lat/lng span.
/// Equivalent with `toSpan()` method call but it would require JsInterop.
/// </summary>
public GoogleMapLatLng Span { get; set; }

/// <summary>
/// Returns if the bounds are empty.
/// Equivalent with `isEmpty()` method call but it would require JsInterop.
/// </summary>
public bool IsEmpty { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Majorsoft.Blazor.Components.Maps.Google
{
/// <summary>
/// Restrictions coordinates for <see cref="GoogleMap"/>
/// NOTE: Google Maps restriction is basically a MAX Zoom level. So it does not allow users to zoom out (zoom level value forced).
/// In order to notify Blazor about the maximum Zoom level two-way binding MUST be used: `@bind-Zoom="_jsMapZoomLevel" @bind-Zoom:event="OnMapZoomLevelChanged"`
/// </summary>
public class GoogleMapRestriction
{
/// <summary>
/// Latitude and Longitude SW/NE corners of the bound.
/// </summary>
public GoogleMapLatLngBounds LatLngBounds { get; set; }

/// <summary>
/// Is restriction strict.
/// </summary>
public bool StrictBounds { get; set; }
}
}
33 changes: 30 additions & 3 deletions src/Majorsoft.Blazor.Components.Maps/Google/GoogleMapService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.JSInterop;

using Microsoft.JSInterop;

using System;
using System.Collections.Generic;
@@ -51,7 +52,8 @@ public async Task InitMapAsync(string apiKey,
Func<GeolocationCoordinate, Task>? mapDragStartCallback = null,
Func<Rect, Task>? mapResizedCallback = null,
Func<Task>? mapTilesLoadedCallback = null,
Func<Task>? mapIdleCallback = null)
Func<Task>? mapIdleCallback = null,
GoogleMapRestriction restriction = null)
{
if(MapContainerId == mapContainerId)
{
@@ -89,7 +91,7 @@ public async Task InitMapAsync(string apiKey,

_dotNetObjectReference = DotNetObjectReference.Create<GoogleMapEventInfo>(info);

await _mapsJs.InvokeVoidAsync("init", apiKey, mapContainerId, _dotNetObjectReference, backgroundColor, controlSize);
await _mapsJs.InvokeVoidAsync("init", apiKey, mapContainerId, _dotNetObjectReference, backgroundColor, controlSize, restriction);
}

public async Task SetCenterAsync(double latitude, double longitude)
@@ -236,6 +238,31 @@ await _mapsJs.InvokeVoidAsync("removeMarkers", MapContainerId,
}
}

public async ValueTask<GoogleMapLatLngBounds> GetBoundsAsync()
{
await CheckJsObjectAsync();
return await _mapsJs.InvokeAsync<GoogleMapLatLngBounds>("getBounds", MapContainerId);
}

public async ValueTask<GoogleMapLatLng> GetCenterAsync()
{
await CheckJsObjectAsync();
return await _mapsJs.InvokeAsync<GoogleMapLatLng>("getCenter", MapContainerId);
}

public async ValueTask<IJSObjectReference> GetDivAsync()
{
await CheckJsObjectAsync();
return await _mapsJs.InvokeAsync<IJSObjectReference>("getDiv", MapContainerId);
}

public async Task AddPolyline(params GoogleMapPolylineOptions[] googleMapPolylineOptions)
{
await CheckJsObjectAsync();
await _mapsJs.InvokeVoidAsync("polylineSetMap", MapContainerId, googleMapPolylineOptions);
}


private async Task CheckJsObjectAsync()
{
if (_mapsJs is null)
43 changes: 37 additions & 6 deletions src/Majorsoft.Blazor.Components.Maps/Google/IGoogleMapService.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@

using System;
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Threading.Tasks;

using Microsoft.JSInterop;

namespace Majorsoft.Blazor.Components.Maps.Google
{
/// <summary>
/// Injectable service to handle Google JavaScript Maps functionalities.
/// Injectable service to handle Google JavaScript Maps functionalities. Available on the instance of <see cref="GoogleMap"/> object ref as well.
/// </summary>
public interface IGoogleMapService : IAsyncDisposable
{
@@ -75,7 +76,8 @@ Task InitMapAsync(string apiKey,
Func<GeolocationCoordinate, Task> mapDragStartCallback = null,
Func<Rect, Task> mapResizedCallback = null,
Func<Task> mapTilesLoadedCallback = null,
Func<Task> mapIdleCallback = null);
Func<Task> mapIdleCallback = null,
GoogleMapRestriction restriction = null);

/// <summary>
/// Sets the center point as coordinates of the Map.
@@ -165,11 +167,40 @@ Task InitMapAsync(string apiKey,
Task CreateCustomControlsAsync(IEnumerable<GoogleMapCustomControl> mapCustomControls);

/// <summary>
/// Creates markers on the Map with InfoWindows on the given position with event callbacks.
/// Creates and removes Markers on the Map with InfoWindows on the given position with event callbacks.
/// </summary>
/// <param name="newMarkers">Enumerable new markers to add</param>
/// <param name="markers">Enumerable markers removed or replaced</param>
/// <returns></returns>
/// <returns>Async task</returns>
Task CreateMarkersAsync(IEnumerable<GoogleMapMarker>? newMarkers, IEnumerable<GoogleMapMarker>? markers);

/// <summary>
/// Returns the lat/lng bounds of the current viewport. If more than one copy of the world is visible,
/// the bounds range in longitude from -180 to 180 degrees inclusive. If the map is not yet initialized or
/// center and zoom have not been set then the result is undefined. For vector maps with non-zero tilt or heading,
/// the returned lat/lng bounds represents the smallest bounding box that includes the visible region of the map's viewport.
/// </summary>
/// <returns>Async task</returns>
ValueTask<GoogleMapLatLngBounds> GetBoundsAsync();

/// <summary>
/// Returns the position displayed at the center of the map. Note that this LatLng object is not wrapped. See LatLng for more information.
/// If the center or bounds have not been set then the result is undefined.
/// </summary>
/// <returns>Async task</returns>
ValueTask<GoogleMapLatLng> GetCenterAsync();

/// <summary>
/// Returns HTMLElement The mapDiv of the map.
/// </summary>
/// <returns>Async task</returns>
ValueTask<IJSObjectReference> GetDivAsync();

/// <summary>
/// Creates and removes Polyline on the Map with given values and event callbacks.
/// </summary>
/// <param name="googleMapPolylineOptions"></param>
/// <returns></returns>
Task AddPolyline(params GoogleMapPolylineOptions[] googleMapPolylineOptions);
}
}
857 changes: 568 additions & 289 deletions src/Majorsoft.Blazor.Components.Maps/Majorsoft.Blazor.Components.Maps.xml

Large diffs are not rendered by default.

83 changes: 79 additions & 4 deletions src/Majorsoft.Blazor.Components.Maps/wwwroot/googleMaps.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export function init(key, elementId, dotnetRef, backgroundColor, controlSize) {
export function init(key, elementId, dotnetRef, backgroundColor, controlSize, restriction) {
if (!key || !elementId || !dotnetRef) {
return;
}

storeElementIdWithDotnetRef(_mapsElementDict, elementId, dotnetRef, backgroundColor, controlSize); //Store map info
storeElementIdWithDotnetRef(_mapsElementDict, elementId, dotnetRef, backgroundColor, controlSize, restriction); //Store map info

let src = "https://maps.googleapis.com/maps/api/js?key=";
let scriptsIncluded = false;
@@ -50,9 +50,25 @@ window.initGoogleMaps = () => {
}

//Create Map
let restrict = null;
if (mapInfo.restriction && mapInfo.restriction.latLngBounds
&& mapInfo.restriction.latLngBounds.northEast && mapInfo.restriction.latLngBounds.southWest) {
restrict =
{
latLngBounds: {
south: mapInfo.restriction.latLngBounds.southWest.lat,
west: mapInfo.restriction.latLngBounds.southWest.lng,
north: mapInfo.restriction.latLngBounds.northEast.lat,
east: mapInfo.restriction.latLngBounds.northEast.lng
},
strictBounds: mapInfo.restriction.strictBounds,
};
}

let map = new google.maps.Map(document.getElementById(elementId), {
backgroundColor: mapInfo.bgColor,
controlSize: mapInfo.ctrSize,
restriction: restrict,
});
map.elementId = elementId;
_mapsElementDict[i].value.map = map;
@@ -265,7 +281,7 @@ window.initGoogleMaps = () => {
};

//Store elementId with .NET Ref
function storeElementIdWithDotnetRef(dict, elementId, dotnetRef, backgroundColor, controlSize) {
function storeElementIdWithDotnetRef(dict, elementId, dotnetRef, backgroundColor, controlSize, restriction) {
let elementFound = false;
for (let i = 0; i < dict.length; i++) {
if (dict[i].key === elementId) {
@@ -276,7 +292,7 @@ function storeElementIdWithDotnetRef(dict, elementId, dotnetRef, backgroundColor
if (!elementFound) {
dict.push({
key: elementId,
value: { ref: dotnetRef, map: null, bgColor: backgroundColor, ctrSize: controlSize }
value: { ref: dotnetRef, map: null, bgColor: backgroundColor, ctrSize: controlSize, restriction: restriction }
});
}
}
@@ -342,6 +358,49 @@ export function panToAddress(elementId, address) {
}
}
}
//get methods
export function getBounds(elementId) {
if (elementId) {
let mapWithDotnetRef = getElementIdWithDotnetRef(_mapsElementDict, elementId);
if (mapWithDotnetRef && mapWithDotnetRef.map) {
let bounds = mapWithDotnetRef.map.getBounds();

let ret = {
Center: convertToLatLng(bounds.getCenter()),
NorthEast: convertToLatLng(bounds.getNorthEast()),
SouthWest: convertToLatLng(bounds.getSouthWest()),
Span: convertToLatLng(bounds.toSpan()),
IsEmpty: bounds.isEmpty(),
};
return ret;
}
}
}
export function getCenter(elementId) {
if (elementId) {
let mapWithDotnetRef = getElementIdWithDotnetRef(_mapsElementDict, elementId);
if (mapWithDotnetRef && mapWithDotnetRef.map) {
let center = mapWithDotnetRef.map.getCenter();

let ret = convertToLatLng(center);
return ret;
}
}
}
export function getDiv(elementId) {
if (elementId) {
let mapWithDotnetRef = getElementIdWithDotnetRef(_mapsElementDict, elementId);
if (mapWithDotnetRef && mapWithDotnetRef.map) {
var ret = mapWithDotnetRef.map.getDiv();
return ret;
}
}
}
function convertToLatLng(latLngObject) {
let ret = { lat: latLngObject.lat(), lng: latLngObject.lng() };
return ret;
}

//set methods
export function setZoom(elementId, zoom) {
if (elementId) {
@@ -554,6 +613,22 @@ function setMarkerData(markerData, marker) {
marker.setZIndex(markerData.zIndex);
}

//Drawing
export function polylineSetMap(elementId, polylineOptions) {
if (elementId && polylineOptions && polylineOptions.length) {
let mapWithDotnetRef = getElementIdWithDotnetRef(_mapsElementDict, elementId);
if (mapWithDotnetRef && mapWithDotnetRef.map) {

for (var i = 0; i < polylineOptions.length; i++) {
let options = polylineOptions[i];

let polyline = new google.maps.Polyline(options);
polyline.setMap(mapWithDotnetRef.map);
}
}
}
}

//Google GeoCoder
export function getAddressCoordinates(elementId, address) {
geocodeAddress(address, function (results) {
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@

using Bunit;

using Majorsoft.Blazor.Components.Common.JsInterop.Navigation;
using Majorsoft.Blazor.Components.Common.JsInterop.Scroll;
using Majorsoft.Blazor.Components.CommonTestsBase;

@@ -18,17 +19,20 @@ public class PermaLinkBlazorServerInitializerTest : ComponentsTestBase<PermaLink
{
private Mock<IPermaLinkWatcherService> _permaLinkWatcherServiceMock;
private Mock<IScrollHandler> _scrollHandlerMock;
private Mock<INavigationHistoryService> _navigationHistoryServiceMock;

[TestInitialize]
public void Init()
{
var logger = new Mock<ILogger<IPermaLinkWatcherService>>();
_permaLinkWatcherServiceMock = new Mock<IPermaLinkWatcherService>();
_scrollHandlerMock = new Mock<IScrollHandler>();
_navigationHistoryServiceMock = new Mock<INavigationHistoryService>();

_testContext.Services.Add(new ServiceDescriptor(typeof(ILogger<IPermaLinkWatcherService>), logger.Object));
_testContext.Services.Add(new ServiceDescriptor(typeof(IPermaLinkWatcherService), _permaLinkWatcherServiceMock.Object));
_testContext.Services.Add(new ServiceDescriptor(typeof(IScrollHandler), _scrollHandlerMock.Object));
_testContext.Services.Add(new ServiceDescriptor(typeof(INavigationHistoryService), _navigationHistoryServiceMock.Object));
_testContext.Services.Add(new ServiceDescriptor(typeof(SingletonComponentService<PermaLinkBlazorServerInitializer>), new SingletonComponentService<PermaLinkBlazorServerInitializer>()));
_testContext.Services.Add(new ServiceDescriptor(typeof(SingletonComponentService<PermalinkBlazorWasmInitializer>), new SingletonComponentService<PermalinkBlazorWasmInitializer>()));
}
Original file line number Diff line number Diff line change
@@ -8,6 +8,31 @@ namespace Majorsoft.Blazor.Components.PermaLink
/// </summary>
public interface IPermaLinkWatcherService : IDisposable
{
/// <summary>
/// Should scroll smoothly to the found permalink element or jump.
/// Note: smooth scroll on element level might not supported by all Browsers.
/// </summary>
bool SmoothScroll { get; set; }

/// <summary>
/// Event handler for parmalinks detected on navigation.
/// </summary>
public event EventHandler<PermalinkDetectedEventArgs> PermalinkDetected;

/// <summary>
/// Modify the current URL with given new peralink value and trigger navigation or just update browser History.
/// </summary>
/// <param name="newPermalink">New peramlink value, NULL will remove permalink part from URL</param>
/// <param name="doNotNavigate">False, will trigger a navigation. When true, just add new URL to the History</param>
void ChangePermalink(string? newPermalink, bool doNotNavigate);

/// <summary>
/// Checks the current URL for permalink again and re-triggers `PermalinkDetected` event if requested.
/// </summary>
/// <param name="triggerEvent">PermalinkDetected should be re-triggered or not</param>
/// <returns>Found permalink value or NULL</returns>
string? CheckPermalink(bool triggerEvent = false);

/// <summary>
/// Starts a navigation watcher which will check for Permalinks in the URLs.
/// </summary>
Loading