I'm learning JSInterop with Blazor.
I would like to get the width and the height of a Blazor page in oder to size à canvas.
I'm using the default Blazor template with the menu bar on the left part of the screen.
On the html part of the Canvas Test 1 page I have:
#page "/canvas_test1"
#inject IJSRuntime JsRuntime
<canvas #ref="canvas1"></canvas>
<button #onclick="SetCanvasSize">Set canvas size</button>
In the code part I have:
ElementReference canvas1;
async Task SetCanvasSize()
{
await JsRuntime.InvokeVoidAsync("SetCanvasSize", canvas1);
}
In the Javascript file I have:
function SetCanvasSize(element) {
ctx = element.getContext('2d');
console.log(window.innerWidth);
console.log(document.documentElement.scrollWidth);
}
But both methodes
window.innerWidth and document.documentElement.scrollWidth
give the width of the entire window not only the page that contains the canvas.
How can I get the width of only the page without the side menu?
Thank you
I think you're a little confused about what a Page is in Blazor. It's basically a component that you provide a route to for navigation. If you include a layout (which is usually set by default), that is in fact part of the page. All the things on the page, like your canvas, are just html elements. It is correctly telling you the size of the window, which you can see will change if you resize your browser.
html elements have the properties "offsetWidth" and "offsetHeight." So, for your use you want to pass the element to measure, and use element.offsetWidth:
function GetCanvasSize(element) {
alert ("Element measurements: " + element.offsetWidth + ", " + element.offsetHeight);
}
You can see more of the members of html elements here, including common methods and events: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth
Note that this list is missing some important things you'd like to know, especially the various touch events which you'll need for dragging elements on mobile clients (mouse events won't work on phones).
You need to get the dimensions of the containing element. You also need to listen for resizing events.
I use a service that can take a ref for any element and listen for changes.
bCore.js
export function listenToWindowResize(dotNetHelper) {
function resizeEventHandler() {
dotNetHelper.invokeMethodAsync('WindowResizeEvent');
}
window.addEventListener("resize", resizeEventHandler);
dotNetHelper.invokeMethodAsync('WindowResizeEvent');
}
export function getBoundingRectangle(element, parm) {
return element.getBoundingClientRect();
}
export function getWindowSizeDetails(parm) {
var e = window, a = 'inner';
if (!('innerWidth' in window)) {
a = 'client';
e = document.documentElement || document.body;
}
let windowSize =
{
innerWidth: e[a + 'Width'],
innerHeight: e[a + 'Height'],
screenWidth: window.screen.width,
screenHeight: window.screen.height
};
return windowSize;
}
JSInteropCoreService.cs
public class JSInteropCoreService : IJSInteropCoreService, IAsyncDisposable
{
private readonly Lazy<Task<IJSObjectReference>> moduleTask;
private bool isResizing;
private System.Timers.Timer resizeTimer;
private DotNetObjectReference<JSInteropCoreService> jsInteropCoreServiceRef;
public JSInteropCoreService(IJSRuntime jsRuntime)
{
this.resizeTimer = new System.Timers.Timer(interval: 25);
this.isResizing = false;
this.resizeTimer.Elapsed += async (sender, elapsedEventArgs) => await DimensionsChanged(sender!, elapsedEventArgs);
this.moduleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>(
identifier: "import", args: "./_content/BJSInteroptCore/bCore.js").AsTask());
}
public event NotifyResizing OnResizing;
public event NotifyResize OnResize;
public async ValueTask InitializeAsync()
{
IJSObjectReference module = await GetModuleAsync();
this.jsInteropCoreServiceRef = DotNetObjectReference.Create(this);
await module.InvokeVoidAsync(identifier: "listenToWindowResize", this.jsInteropCoreServiceRef);
this.BrowserSizeDetails = await module.InvokeAsync<BrowserSizeDetails>(identifier: "getWindowSizeDetails");
}
public async ValueTask<BrowserSizeDetails> GetWindowSizeAsync()
{
IJSObjectReference module = await GetModuleAsync();
return await module.InvokeAsync<BrowserSizeDetails>(identifier: "getWindowSizeDetails");
}
public async ValueTask<ElementBoundingRectangle> GetElementBoundingRectangleAsync(ElementReference elementReference)
{
IJSObjectReference module = await GetModuleAsync();
return await module.InvokeAsync<ElementBoundingRectangle>(identifier: "getBoundingRectangle", elementReference);
}
[JSInvokable]
public ValueTask WindowResizeEvent()
{
if (this.isResizing is not true)
{
this.isResizing = true;
OnResizing?.Invoke(this.isResizing);
}
DebounceResizeEvent();
return ValueTask.CompletedTask;
}
public BrowserSizeDetails BrowserSizeDetails { get; private set; } = new BrowserSizeDetails();
private void DebounceResizeEvent()
{
if (this.resizeTimer.Enabled is false)
{
Task.Run(async () =>
{
this.BrowserSizeDetails = await GetWindowSizeAsync();
isResizing = false;
OnResizing?.Invoke(this.isResizing);
OnResize?.Invoke();
});
this.resizeTimer.Restart();
}
}
private async ValueTask DimensionsChanged(object sender, System.Timers.ElapsedEventArgs e)
{
this.resizeTimer.Stop();
this.BrowserSizeDetails = await GetWindowSizeAsync();
isResizing = false;
OnResizing?.Invoke(this.isResizing);
OnResize?.Invoke();
}
public async ValueTask DisposeAsync()
{
if (moduleTask.IsValueCreated)
{
IJSObjectReference module = await GetModuleAsync();
await module.DisposeAsync();
}
}
private async Task<IJSObjectReference> GetModuleAsync()
=> await this.moduleTask.Value;
}
public class ElementBoundingRectangle
{
public double X { get; set; }
public double Y { get; set; }
public double Width { get; set; }
public double Height { get; set; }
public double Top { get; set; }
public double Right { get; set; }
public double Bottom { get; set; }
public double Left { get; set; }
}
public class BrowserSizeDetails
{
public double InnerWidth { get; set; }
public double InnerHeight { get; set; }
public int ScreenWidth { get; set; }
public int ScreenHeight { get; set; }
}
I debounce the resize events using a 25ms interval to prevent lag.
Related
I am writing a modal component for blazor but i am struggeling to find a solution which fulfills the requirement to do both at the same time:
close ONLY when clicked outside modal content
update C# object state (Visibility flag) accordingly
scenario 1
If i use the C# based approach with #onclick="WrapperClicked" i can update the Visibility state very easy, but struggle to get the DOM click event and therefore cannot distinguish between a wrapper and a wrapper content click.
The c# MouseEventArgs do not contain properties to distinguish the clicked dom element.
scenario 2
Is based around uncommenting the code for
private Dictionary<string, object> ComponentValues()
{
var values = new Dictionary<string, object>();
// if(CloseOnModalFrameClickInternal)
// values.Add("onclick", "Amusoft.Components.ModalDialog.closeEvent(this, event);");
return values;
}
With this version it is simple to get access to the dom click event - but i cannot pass a DotNetObjectReference to that handler, to be able to call back into c# and update my components state.
Question
Does anyone have ideas how to resolve this deep interop scenario?
typescript code to distinguish wrapper click from wrapper content click:
public static closeEvent(dotNetHelper: any, event: MouseEvent): void {
console.log(event);
console.log(dotNetHelper);
let target = event.target as HTMLElement;
if(target != null && target?.classList?.contains("amu-modal-wrapper")){
// element.style.setProperty("display", "none");
console.log("breakpoint landed");
// valueOfReference.invokeMethodAsync("JsSetVisibility", false);
}
}
Additional Scss code compared to default code:
.amu-modal-wrapper {
background-color: rgba(0, 0, 0, 0.5);
position: absolute;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 20px;
.amu-modal-content {
width: 100%;
height: 100%;
background-color: orange;
}
}
Modal component code:
#using Microsoft.JSInterop
#implements IDisposable
<div #ref="wrapper" class="amu-modal-wrapper" #attributes="ComponentValues()" #onclick="WrapperClicked" style="padding: #(Padding)px; display: #(VisibleInternal ? "block" : "none")">
<div class="amu-modal-content">
<h3>#Headline</h3>
#ChildContent
</div>
</div>
#code {
private ElementReference wrapper;
private DotNetObjectReference<ModalDialog> _self;
[Parameter]
public string Headline
{
get => HeadlineInternal;
set => HeadlineInternal = value;
}
[Parameter]
public EventCallback<string> HeadlineChanged { get; set; }
private string _headlineInternal;
private string HeadlineInternal
{
get { return _headlineInternal; }
set
{
if (EqualityComparer<string>.Default.Equals(_headlineInternal, value))
return;
_headlineInternal = value;
HeadlineChanged.InvokeAsync(value);
}
}
[Parameter]
public RenderFragment ChildContent { get; set; }
[Inject]
public IJSRuntime JsRuntime { get; set; }
[Parameter]
public int Padding { get; set; } = 100;
[Parameter]
public bool Visible
{
get => VisibleInternal;
set => VisibleInternal = value;
}
[Parameter]
public EventCallback<bool> VisibleChanged { get; set; }
private bool _visibleInternal;
private bool VisibleInternal
{
get { return _visibleInternal; }
set
{
if (EqualityComparer<bool>.Default.Equals(_visibleInternal, value))
return;
_visibleInternal = value;
VisibleChanged.InvokeAsync(value);
}
}
[Parameter]
public bool CloseOnModalFrameClick
{
get => CloseOnModalFrameClickInternal;
set => CloseOnModalFrameClickInternal = value;
}
private Dictionary<string, object> ComponentValues()
{
var values = new Dictionary<string, object>();
// if(CloseOnModalFrameClickInternal)
// values.Add("onclick", "Amusoft.Components.ModalDialog.closeEvent(this, event);");
return values;
}
private bool CloseOnModalFrameClickInternal { get; set; } = true;
public void Hide()
{
VisibleInternal = false;
StateHasChanged();
}
public void Show()
{
VisibleInternal = true;
StateHasChanged();
}
protected override void OnInitialized()
{
_self = DotNetObjectReference.Create(this);
base.OnInitialized();
}
protected override void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);
if (firstRender)
JsRuntime.InvokeVoidAsync("Amusoft.Components.ModalDialog.initialize", wrapper);
}
public void Dispose()
{
_self?.Dispose();
}
private Task WrapperClicked(MouseEventArgs arg)
{
JsRuntime.InvokeVoidAsync("Amusoft.Components.ModalDialog.closeEvent", _self, arg);
return Task.CompletedTask;
}
}
You dont need Java.
<div class="modal-outer" #onclick="OnBackgroundClicked">
<div class="modal-inner" #onclick:stopPropagation="true">
From my repo
It is now a free nuget package as of about 24hrs ago
I am loading images from Wikipedia into a Grid view. For the most part this is working correctly. Because there could possible be up to 200 or more images being loaded I am try to run it in a new thread. I see a definite delay when scrolling from my Album tab to the Artist tab that is loading the images. I am also see some lag as images are still getting load while scrolling up and down the list. Also when I scroll back to the top of the list place holders that previously occupied by the default image because I am unable to get an image from Wikipedia are now occupied by images from another artist.
When I scroll back to the song list and then back to the artist list the view is reset but it still has a lot of delay when going into the artist tab.
This image is what the screen looks like when first entering the Artist tab.
This image is what the screen looks like after scrolling to the bottom of the list and back to the top.
As you can see the <unknow. and AJR have had their default image replaced.
Here is my code that I am calling to load the images from Wikipedia.
#Override
public void onBindViewHolder(#NonNull ARV holder, int position) {
Artist artist = artistList.get(position);
if(artist!=null) {
holder.artistName.setText(artist.artistName);
String bandName = artist.artistName;
bandName = bandName.replace(' ','_');
try {
String imageUrl = cutImg(getUrlSource("https://en.wikipedia.org/w/api.php?action=query&titles="+bandName+"&prop=pageimages&format=json&pithumbsize=250"));
URL url = new URL(imageUrl);
ImageLoader.getInstance().displayImage(imageUrl, holder.artistImage,
new DisplayImageOptions.Builder().cacheInMemory(true).showImageOnLoading(R.drawable.album)
.resetViewBeforeLoading(true).build());
} catch (IOException e) {
e.printStackTrace();
}
/*ImageLoader.getInstance().displayImage(getCoverArtPath(context,artist.id),holder.artistImage,
new DisplayImageOptions.Builder().cacheInMemory(true).showImageOnLoading(R.drawable.album)
.resetViewBeforeLoading(false).build());*/
}
}
private StringBuilder getUrlSource(String site) throws IOException {
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
StrictMode.setThreadPolicy(policy);
URL localUrl = null;
localUrl = new URL(site);
URLConnection conn = localUrl.openConnection();
BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
String line = "";
String html;
StringBuilder ma = new StringBuilder();
while ((line = reader.readLine()) != null) {
ma.append(line);
Log.i(ContentValues.TAG, "StringBuilder " + ma);
}
Log.i(ContentValues.TAG, "Final StringBuilder " + ma);
return ma;
}
public static String cutImg(StringBuilder split){
int start=split.indexOf("\"source\":")+new String("\"source\":\"").length();
split.delete(0, start);
split.delete(split.indexOf("\""), split.length());
Log.i(ContentValues.TAG, "StringBuilder " + split);
return split.toString();
}
Here is the code that is call the Artist Fragment.
public class ArtistFragment extends Fragment {
int spanCount = 3; // 2 columns
int spacing = 20; // 20px
boolean includeEdge = true;
private RecyclerView recyclerView;
private ArtistAdapter adapter;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_artist, container, false);
recyclerView = view.findViewById(R.id.artistFragment);
recyclerView.setLayoutManager(new GridLayoutManager(getActivity(), 3));
Thread t = new Thread()
{
public void run()
{
// put whatever code you want to run inside the thread here.
new LoadData().execute("");
}
};
t.start();
return view;
}
public class LoadData extends AsyncTask<String, Void, String> {
#Override
protected String doInBackground(String... strings) {
if(getActivity()!=null) {
adapter=new ArtistAdapter(getActivity(),new ArtistLoader().artistList(getActivity()));
}
return "Executed";
}
#Override
protected void onPostExecute(String s) {
recyclerView.setAdapter(adapter);
if(getActivity()!=null) {
recyclerView.addItemDecoration(new GridSpacingItemDecoration(spanCount, spacing, includeEdge));
}
}
#Override
protected void onPreExecute() {
super.onPreExecute();
}
}
}
I have also tried this using Picasso using the following code:
bandName = artist.artistName;
bandName = bandName.replace(' ','_');
try {
String imageUrl = cutImg(getUrlSource("https://en.wikipedia.org/w/api.php?action=query&titles="+bandName+"&prop=pageimages&format=json&pithumbsize=250"));
URL url = new URL(imageUrl);
Picasso.get().load(imageUrl).placeholder(R.drawable.album)
.error(R.drawable.artistdefault).into(holder.artistImage);
} catch (IOException e) {
e.printStackTrace();
}
The results are pretty much the same as when I used Android-Universal-Image-Loader. I have been try for several days to fix this, I have tried several different examples that I found on Stack overflow but none of them seem to resolve the issues I am seeing. I am hoping that someone will be able to identify what I am doing incorrectly.
Thanks in advance.
ArtistFragmentconverted to Kotlin
class ArtistFragment : Fragment() {
var spanCount = 3 // 2 columns
var spacing = 20 // 20px
var includeEdge = true
var retrofit: Retrofit? = null
var wikiService: WikiService? = null
var adapter: ArtistAdapter? = null
private var recyclerView: RecyclerView? = null
private var viewModelJob = Job()
private val viewModelScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private var progress_view: ProgressBar? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_artist, container, false)
recyclerView = view.findViewById(R.id.artistFragment)
recyclerView?.setLayoutManager(GridLayoutManager(activity, 3))
progress_view = view.findViewById(R.id.progress_view)
initWikiService()
initList()
//LoadData().execute("")
return view
}
override fun onDestroy() {
super.onDestroy()
viewModelJob.cancel()
}
private fun initList() {
recyclerView!!.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, includeEdge))
adapter = ArtistAdapter(this)
adapter?.items = ArrayList()
adapter?.listener = this
recyclerView?.adapter = adapter
viewModelScope.launch {
progress_view.visibility = View.VISIBLE
val wikiPages = getWikiPages()
adapter?.items = wikiPages
progress_view?.visibility = View.GONE
}
}
private suspend fun getWikiPages(): ArrayList<Artist> {
val newItems = ArrayList<Artist>()
withContext(Dispatchers.IO) {
ArtistData.artists.map { artist ->
async { wikiService?.getWikiData(artist) }
}.awaitAll().forEach { response ->
val pages = response?.body()?.query?.pages
pages?.let {
for (page in pages) {
val value = page.value
val id = value.pageid?.toLong() ?: value.title.hashCode().toLong()
val title = value.title ?: "Unknown"
val url = value.thumbnail?.source
newItems.add(Artist(id, title, albumCount = 0, songCount = 0, artistUrl = url!!))
}
}
}
}
return newItems
}
private fun initWikiService() {
retrofit = Retrofit.Builder()
.baseUrl("https://en.wikipedia.org/")
.addConverterFactory(GsonConverterFactory.create())
.build()
wikiService = retrofit?.create(WikiService::class.java)
}
I believe I have resolved most of the issues I was previously seeing I am now down to the following problems:
Artist.item.map { artist -> - Not sure how this should be called, Unresolved reference: item
}.awaitAll().forEach { response -> = forEach is telling me Overload resolution ambiguity. All these functions match.
public inline fun Iterable<TypeVariable(T)>.forEach(action: (TypeVariable(T)) → Unit): Unit defined in kotlin.collections
public inline fun <K, V> Map<out TypeVariable(K), TypeVariable(V)>.forEach(action: (Map.Entry<TypeVariable(K), TypeVariable(V)>) → Unit): Unit defined in kotlin.collections
newItems.add(Artist(id, title, url)) - I know that the variables for the Artist Model need to go here, but when I put them there they are unresolved.
I have reworked the ArtistAdapter not sure if it is correct though.
class ArtistAdapter(private val context: ArtistFragment, private val artistList: List<Artist>?) : RecyclerView.Adapter<ArtistAdapter.ARV>() {
private var dimension: Int = 64
init {
val density = context.resources.displayMetrics.density
dimension = (density * 64).toInt()
hasStableIds()
}
var items: MutableList<Artist> = ArrayList()
set(value) {
field = value
notifyDataSetChanged()
}
var listener: Listener? = null
interface Listener {
fun onItemClicked(item: Artist)
abstract fun ArtistAdapter(context: ArtistFragment): ArtistAdapter
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ARV {
return ARV(LayoutInflater.from(parent.context).inflate(R.layout.artist_gride_item, parent,
false))
}
override fun onBindViewHolder(holder: ARV, position: Int) {
holder.onBind(getItem(position))
}
private fun getItem(position: Int): Artist = items[position]
override fun getItemId(position: Int): Long = items[position].id
override fun getItemCount(): Int {
return artistList?.size ?: 0
}
inner class ARV(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
private val artistNameView: TextView = itemView.findViewById(R.id.artistName)
private val artistAlbumArtView: SquareCellView = itemView.findViewById(R.id.artistAlbumArt)
fun onBind(item: Artist) {
artistNameView.text=item.artistName
if(item.artistURL!=null) {
Picasso.get()
.load(item.artistURL)
.resize(dimension, dimension)
.centerCrop()
.error(R.drawable.artistdefualt)
.into(artistAlbumArtView)
} else {
artistAlbumArtView.setImageResource(R.drawable.artistdefualt)
}
itemView.setOnClickListener(this)
}
override fun onClick(view: View) {
val artistId = artistList!![bindingAdapterPosition].id
val fragmentManager = (context as AppCompatActivity).supportFragmentManager
val transaction = fragmentManager.beginTransaction()
val fragment: Fragment
transaction.setCustomAnimations(R.anim.layout_fad_in, R.anim.layout_fad_out,
R.anim.layout_fad_in, R.anim.layout_fad_out)
fragment = ArtistDetailsFragment.newInstance(artistId)
transaction.hide(context.supportFragmentManager
.findFragmentById(R.id.main_container)!!)
transaction.add(R.id.main_container, fragment)
transaction.addToBackStack(null).commit()
}
}
}
Logcat Snippet
java.lang.NoSuchMethodError: No static method metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; in class Ljava/lang/invoke/LambdaMetafactory; or it
s super classes (declaration of 'java.lang.invoke.LambdaMetafactory' appears in /apex/com.android.art/javalib/core-oj.jar)
at okhttp3.internal.Util.<clinit>(Util.java:87)
at okhttp3.internal.Util.skipLeadingAsciiWhitespace(Util.java:321)
at okhttp3.HttpUrl$Builder.parse(HttpUrl.java:1313)
at okhttp3.HttpUrl.get(HttpUrl.java:917)
at retrofit2.Retrofit$Builder.baseUrl(Retrofit.java:506)
at com.rvogl.androidaudioplayer.fragments.ArtistFragment.initWikiService(ArtistFragment.kt:103)
at com.rvogl.androidaudioplayer.fragments.ArtistFragment.onCreateView(ArtistFragment.kt:43)
It looks to me as a known bug with Picasso.
Try to load default image manually so it won't be replaced with cached one.
Update 14.10.20:
I think the main problem is that you load network content in adapter in rather ineffective way. I suggest to form a list of all urls at first, leaving only image load in adapter.
Also reccomend you to use rerofit2 for network calls and something for async work instead of AsyncTask: rxJava, courutines, flow etc.
I created a sample project to load data async using retrofit2+coroutines.
In activity:
private val viewModelScope = CoroutineScope(Dispatchers.Main)
private fun initWikiService() {
retrofit = Retrofit.Builder()
.baseUrl("https://en.wikipedia.org/")
.addConverterFactory(GsonConverterFactory.create())
.build()
wikiService = retrofit?.create(WikiService::class.java)
}
private fun initList() {
viewModelScope.launch {
val wikiPages = getWikiPages()
adapter?.items = wikiPages
}
}
private val viewModelScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private suspend fun getWikiPages(): ArrayList<Item> {
val newItems = ArrayList<Item>()
withContext(IO) {
ArtistData.artists.map { artist ->
async { wikiService?.getWikiData(artist) }
}.awaitAll().forEach { response ->
val pages = response?.body()?.query?.pages
pages?.let {
for (page in pages) {
val value = page.value
val id = value.pageid?.toLong() ?: value.title.hashCode().toLong()
val title = value.title ?: "Unknown"
val url = value.thumbnail?.source
newItems.add(Item(id, title, url))
}
}
}
}
return newItems
}
In viewHolder:
fun onBind(item: Item) {
if (item.url != null) {
Picasso.get()
.load(item.url)
.resize(dimension, dimension)
.centerCrop()
.error(R.drawable.ic_baseline_broken_image_24)
.into(pictureView)
} else {
pictureView.setImageResource(R.drawable.ic_baseline_image_24)
}
}
In adapter: add hasStableIds() to constructor and override getItemId method:
init {
hasStableIds()
}
override fun getItemId(position: Int): Long = items[position].id
Retrofit Service:
interface WikiService {
#GET("/w/api.php?action=query&prop=pageimages&format=json&pithumbsize=250")
suspend fun getWikiData(#Query("titles") band: String): Response<WikipediaResponse?>
}
How can I run some Javascript functions in a Blazor app, without using the JS interop or jQuery? Just plain old Javascript functions that interact with the DOM, independently of Blazor.
I added my script right before the closing </body> tag:
<script src="app.js"></script>
And in app.js I have the following:
var elements = document.querySelectorAll(".some-element");
elements.forEach(function (element) {
element.addEventListener("click", (e) => {
alert("Hello");
});
});
Of course the selector finds no element. I'm guessing they aren't yet present in the DOM at that point? How can I run that script without using the JS interop or jQuery?
If your okay with using jquery you can use an on handler, which will apply to dynamically added elements
$("body").on("click", ".some-element", function(){
alert("Hello");
});
You can do natively too, but seems sketchy IMHO:
document.querySelector('body').addEventListener('click', function(event) {
if (event.target.className.toLowerCase() === 'some-element') {
alert("Hello");
}
});
Note you have weird javascripty things to worry about now like z-index if the click event actually gets up to the body, but it should work for simple stuff.
Main question is WHY would you want to do this in Blazor - whole point is you can use C# events instead you crazy person!!
Create a page like this:
<div height="#($"{Height}px")" width="#($"{Width}px")">
Test
</div>
<button #onclick="DoubleSize">Double Size</button>
#code
{
public int Width { get; set; } = 50;
public int Height { get; set; } = 50;
private void DoubleSize()
{
Width = Width * 2;
Height = Height * 2;
}
}
On render the height and width are 50px:
Then you click the Button, and they change to 100px:
Similarly with CSS if that's what you were talking about:
<div style="width: #($"{Width}px"); height: #($"{Height}px")">
Test
</div>
This is interacting with the properties of the DOM elements without using Javascript... you can do this with any property. You don't need to use Javascript - you just bind them to your properties in C#...
You shouldn't need to 'retrieve the final properties of the rendered DOM elements' as you should be controlling them from C# and not worrying about the DOM
I'm pretty new in Blazor, so sorry if it's not the correct way to do it in Balzor. But I think that you need always use JS interop to use JavaScript function. You want to execute some script, but the question is: when do you want to execute the script? I imagine you want to execute an action after navigate to a page, after make a click in a button... and all this events happens in Blazor
If you ask about relationate an blazor element with the doom you need use #ref
A little example. You create a .js like
var auxiliarJs = auxiliarJs || {};
auxiliarJs.getBoundingClientRect = function (elementRef) {
let result = elementRef? elementRef.getBoundingClientRect():
{left: 0,top: 0,right: 0,bottom: 0,x: 0,y: 0,width: 0,height: 0 };
return result;
}
auxiliarJs.executeFunction = function (elementRef, funciones) {
let res = null;
try {
if (Array.isArray(funciones)) {
functiones.forEach(funcion => {
elementRef[funcion]()
});
}
else
res = elementRef[funciones]();
}
catch (e) { }
if (res)
return res;
}
auxiliarJs.setDocumentTitle = function (title) {
document.title = title;
};
And a service.cs with his interface
public interface IDocumentService
{
Task<ClientRect> getBoundingClientRect(ElementReference id);
Task setDocumentTitle(string title);
Task<JsonElement> executeFunction(ElementReference id, string funcion);
Task executeFunction(ElementReference id, string[] funciones);
}
public class DocumentService:IDocumentService
{
private IJSRuntime jsRuntime;
public DocumentService(IJSRuntime jsRuntime)
{
this.jsRuntime = jsRuntime;
}
public Dictionary<string, object> JSonElementToDictionary(JsonElement result)
{
Dictionary<string, object> obj = new Dictionary<string, object>();
JsonProperty[] enumerador = result.EnumerateObject().GetEnumerator().ToArray();
foreach (JsonProperty prop in enumerador)
{
obj.Add(prop.Name, prop.Value);
}
return obj;
}
public async Task<ClientRect> getBoundingClientRect(ElementReference id)
{
return await jsRuntime.InvokeAsync<ClientRect>("auxiliarJs.getBoundingClientRect", id);
}
public async Task setDocumentTitle(string title)
{
await jsRuntime.InvokeVoidAsync("auxiliarJs.setDocumentTitle", title);
}
public async Task<JsonElement> executeFunction(ElementReference id,string funcion)
{
var result= await jsRuntime.InvokeAsync<JsonElement>
("auxiliarJs.executeFunction", id, funcion);
return result;
}
public async Task executeFunction(ElementReference id, string[] funciones)
{
await jsRuntime.InvokeVoidAsync("auxiliarJs.executeFunction", id, funciones);
}
}
public class ClientRect
{
public float left{ get; set; }
public float top { get; set; }
public float right { get; set; }
public float bottom { get; set; }
public float x { get; set; }
public float y { get; set; }
public float width { get; set; }
public float height { get; set; }
}
Well, you inject the service as usual in program.cs
public static async Task Main(string[] args){
....
builder.Services.AddSingleton<IDocumentService, DocumentService>();
}
And in your component razor
#inject IDocumentService document
<div #ref="mydiv"></div>
<input #ref="myinput">
<button #onclick="click">click</button>
#code{
private ElementReference mydiv;
private ElementReference myinput;
click(){
ClientRect rect = await document.getBoundingClientRect(mydiv);
document.setDocumentTitle("New Title");
document.executeFunction(myinput 'focus')
}
}
Blazor "overwrites" all attached JS Events during render process
window.attachHandlers = () => {
var elements = document.querySelectorAll(".some-element");
elements.forEach(function (element) {
element.addEventListener("click", (e) => {
alert("Hello");
});
});
and in razor page
protected override void OnAfterRender(bool firstRender)
{
if(firstRender)
{
JSRuntime.InvokeVoidAsync("attachHandlers");
}
}
I need to do 3 things with the CardsDriver: using the default constructor Card() display a default card. Then, take a user's int and display a Card object using the parameterized constructor Card(int n) (Using mod%). Finally use showDeck() to display all 52 cards. (As you can tell I kinda did this using the enhanced for loops at the end, it works but I do not think it is the best way.)
Would really like some help with at least the second problem. I just need to understand what I need to implement..
package cards;
import java.util.Scanner;
public class CardsDriver
{
private static Scanner keyboard = new Scanner(System.in);
public static void main (String[] args)
{
boolean another;
int n;
Card card = new Card();
do
{
System.out.println("Type a non-negative integer. Type -1 to
stop.");
n = keyboard.nextInt( );
if (n == -1)
{
another = false;
}
else
{
another = true;
}
} while (another == true);
System.out.println("All 52 Cards Follow:");
showDeck( );
}
private static int getNextInt()
{
return keyboard.nextInt();
}
private static void showDeck()
{
for (Face face : Face.values())
{
for (Suit suit : Suit.values())
System.out.println("The " + face + " of " + suit);
}
}
}
and here is my Card class
package cards;
public class Card {
private Face face;
private Suit suit;
public Card() {
face = Face.ACE;
suit = Suit.CLUBS;
}
public Card(Card existingCard) {
this.face = existingCard.face;
this.suit = existingCard.suit;
}
public Card(int n) {
face = Face.values()[n % 13];
suit = Suit.values()[n % 4];
}
public String toString() {
return "the" + face + "of" + suit;
}
}
I want to use Google's Text-To-Speech API in an Android app but I only could find the way to do it from web (Chrome). This is my first attempt to play "Hello world" from the app.
playTTS is the onClick and it is been executed, but no sound is played. Is there any JS/Java library I need to import? Is it possible to generate an audio file from it?
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myBrowser = new WebView(this);
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.add(R.id.container, new PlaceholderFragment()).commit();
}
}
public void playTTS(View view) {
myBrowser.loadUrl("javascript:speechSynthesis.speak(
SpeechSynthesisUtterance('Hello World'))");
}
In Android java code your Activity/other Class should implement TextToSpeech.OnInitListener. You will get a TextToSpeech instance by calling TextToSpeech(context, this). (Where context refers to your application's Context -- can be this in an Activity.) You will then receive a onInit() callback with status which tells whether the TTS engine is available or not.
You can talk by calling tts.speak(textToBeSpoken, TextToSpeech.QUEUE_FLUSH, null) or tts.speak(textToBeSpoken, TextToSpeech.QUEUE_ADD, null). The first one will interrupt any "utterance" that is still being spoken and the latter one will add the new "utterance" to a queue. The last parameter is not mandatory. It could be an "utterance id" defined by you in case you want to monitor the TTS status by setting an UtteranceProgressListener. (Not necessary)
In Java code a simple "TTS talker" class could be something like:
public class MyTtsTalker implements TextToSpeech.OnInitListener {
private TextToSpeech tts;
private boolean ttsOk;
// The constructor will create a TextToSpeech instance.
MyTtsTalker(Context context) {
tts = new TextToSpeech(context, this);
}
#Override
// OnInitListener method to receive the TTS engine status
public void onInit(int status) {
if (status == TextToSpeech.SUCCESS) {
ttsOk = true;
}
else {
ttsOk = false;
}
}
// A method to speak something
#SuppressWarnings("deprecation") // Support older API levels too.
public void speak(String text, Boolean override) {
if (ttsOk) {
if (override) {
tts.speak(text, TextToSpeech.QUEUE_FLUSH, null);
}
else {
tts.speak(text, TextToSpeech.QUEUE_ADD, null);
}
}
}
}
Code for TTS:
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import android.util.Log;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import android.widget.Toast;
import java.util.HashMap;
import java.util.Locale;
public class KiwixTextToSpeech {
public static final String TAG_ASKQ = "askq;
private Context context;
private OnSpeakingListener onSpeakingListener;
private WebView webView;
private TextToSpeech tts;
private boolean initialized = false;
/**
* Constructor.
*
* #param context the context to create TextToSpeech with
* #param webView {#link android.webkit.WebView} to take contents from
* #param onInitSucceedListener listener that receives event when initialization of TTS is done
* (and does not receive if it failed)
* #param onSpeakingListener listener that receives an event when speaking just started or
* ended
*/
public KiwixTextToSpeech(Context context, WebView webView,
final OnInitSucceedListener onInitSucceedListener,
final OnSpeakingListener onSpeakingListener) {
Log.d(TAG_ASKQ, "Initializing TextToSpeech");
this.context = context;
this.onSpeakingListener = onSpeakingListener;
this.webView = webView;
this.webView.addJavascriptInterface(new TTSJavaScriptInterface(), "tts");
initTTS(onInitSucceedListener);
}
#TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
private void initTTS(final OnInitSucceedListener onInitSucceedListener) {
tts = new TextToSpeech(context, new TextToSpeech.OnInitListener() {
#Override
public void onInit(int status) {
if (status == TextToSpeech.SUCCESS) {
Log.d(TAG_ASKQ, "TextToSpeech was initialized successfully.");
initialized = true;
onInitSucceedListener.onInitSucceed();
} else {
Log.e(TAG_ASKQ, "Initilization of TextToSpeech Failed!");
}
}
});
tts.setOnUtteranceProgressListener(new UtteranceProgressListener() {
#Override
public void onStart(String utteranceId) {
}
#Override
public void onDone(String utteranceId) {
Log.e(TAG_ASKQ, "TextToSpeech: " + utteranceId);
onSpeakingListener.onSpeakingEnded();
}
#Override
public void onError(String utteranceId) {
Log.e(TAG_ASKQ, "TextToSpeech: " + utteranceId);
onSpeakingListener.onSpeakingEnded();
}
});
}
/**
* Reads the currently selected text in the WebView.
*/
public void readSelection() {
webView.loadUrl("javascript:tts.speakAloud(window.getSelection().toString());", null);
}
/**
* Starts speaking the WebView content aloud (or stops it if TTS is speaking now).
*/
public void readAloud() {
if (tts.isSpeaking()) {
if (tts.stop() == TextToSpeech.SUCCESS) {
onSpeakingListener.onSpeakingEnded();
}
} else {
Locale locale = LanguageUtils.ISO3ToLocale(ZimContentProvider.getLanguage());
int result;
if (locale == null
|| (result = tts.isLanguageAvailable(locale)) == TextToSpeech.LANG_MISSING_DATA
|| result == TextToSpeech.LANG_NOT_SUPPORTED) {
Log.d(TAG_ASKQ, "TextToSpeech: language not supported: " +
ZimContentProvider.getLanguage() + " (" + locale.getLanguage() + ")");
Toast.makeText(context,
context.getResources().getString(R.string.tts_lang_not_supported),
Toast.LENGTH_LONG).show();
} else {
tts.setLanguage(locale);
// We use JavaScript to get the content of the page conveniently, earlier making some
// changes in the page
webView.loadUrl("javascript:" +
"body = document.getElementsByTagName('body')[0].cloneNode(true);" +
// Remove some elements that are shouldn't be read (table of contents,
// references numbers, thumbnail captions, duplicated title, etc.)
"toRemove = body.querySelectorAll('sup.reference, #toc, .thumbcaption, " +
" title, .navbox');" +
"Array.prototype.forEach.call(toRemove, function(elem) {" +
" elem.parentElement.removeChild(elem);" +
"});" +
"tts.speakAloud(body.innerText);");
}
}
}
/**
* Returns whether the TTS is initialized.
*
* #return <code>true</code> if TTS is initialized; <code>false</code> otherwise
*/
public boolean isInitialized() {
return initialized;
}
/**
* Releases the resources used by the engine.
*
* #see android.speech.tts.TextToSpeech#shutdown()
*/
public void shutdown() {
tts.shutdown();
}
/**
* The listener which is notified when initialization of the TextToSpeech engine is successfully
* done.
*/
public interface OnInitSucceedListener {
public void onInitSucceed();
}
/**
* The listener that is notified when speaking starts or stops (regardless of whether it was a
* result of error, user, or because whole text was read).
*
* Note that the methods of this interface may not be called from the UI thread.
*/
public interface OnSpeakingListener {
public void onSpeakingStarted();
public void onSpeakingEnded();
}
private class TTSJavaScriptInterface {
#JavascriptInterface
#SuppressWarnings("unused")
public void speakAloud(String content) {
String[] lines = content.split("\n");
for (int i = 0; i < lines.length - 1; i++) {
String line = lines[i];
tts.speak(line, TextToSpeech.QUEUE_ADD, null);
}
HashMap<String, String> params = new HashMap<>();
// The utterance ID isn't actually used anywhere, the param is passed only to force
// the utterance listener to be notified
params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "kiwixLastMessage");
tts.speak(lines[lines.length - 1], TextToSpeech.QUEUE_ADD, params);
if (lines.length > 0) {
onSpeakingListener.onSpeakingStarted();
}
}
}
}