Záznam ze Vzdělávacího okénka HAVIT, kde Jirka Kanda ukazoval, jak volat z Blazoru JavaScript a z JavaScriptu Blazor.
Tag Archives: JavaScript
Označování textu v HTML aneb na počtvrté správně a po svém
Cíle
- Označit (resp. zvýraznit) text v prohlížeči (Chrome, Firefox, IE, Edge) a to i v mobilních verzích aplikace (Android, iOS, Windows Phone)
- Uložit serializovanou podobu označení do úložiště pro pozdější obnovení označení (například při návratu na stránku)
- Při novém načtení stránky (render stránky), označit text
Poprvé
Musím se přiznat, že k prvnímu pokusu o označení jsem pouze přišel s tím, že už to je implementované správně a funguje. Nefungovalo. V daném HTML byli vytvořené jakési „stropy“ (offsety), reprezentované prostým span HTML elementem s číselnou hodnotu v datasetu.
Tento kód zde raději ani nesdílím, neb bych nerad, aby se čtenáři udělalo nevolno.
Nebylo možné označování přes více různých elementů – původně počítalo s označením pouze v odstavcích, ale tabulky, divy nebo obrázky to ignorovalo.
Takhle to nepůjde.
Podruhé
Většinou nejprve hledám po internetu, jestli už někdo neřešil podobný problém. Měl jsem štěstí – řešil. Narazil jsem na knihovnu rangy.js. Má svou wiki, kde je krásně popsáno API, takže jsem se mohl podívat, co tato knihovna umí.
rangy.js má v sobě modul pro zvýraznění (Highlighter module), který má dvě implementace:
- textContent
- výhody: jednoduchý, rychlý algoritmus
- nevýhody: změny DOMu mají za následek, označení něčeho jiného než bylo před změnou DOMu označeno, i jakýkoli bilý znak (white-space) měl za následek mutaci označení
- TextRange
- výhody: odolný proti zápisu více bílých znaků (white-space)
- nevýhody: extrémně pomalý
Implmentace „textContent“ nemohla být použita, protože mobilní aplikace renderovala HTML trochu jinak a to i přesto, že bylo použito Chromium (jádro Chrome tzn. stejný render engine).
Zkusil jsem implementaci „TextRange“. Nepoužitelné. Mobilní aplikaci trvalo až 10s než vyrenderuje označené HTML.
I když bylo označení vždy přesné, tak z výkonových důvodu jsem od něj upustil.
Potřetí
Tento postup, už jsem vymýšlel já. Spoočíval v serializaci Range objektu velmi jednoduchou metodou. Range objekt má v sobě startContainer, startOffset, endContainer a endOffset. Serializace probíhá tak, že se vygeneruje validní selektor elementu startContainer a endContainer relativně ke zvolenému HTML elementu (například k body) a spočítá se startOffset a endOffset v daných start/end kontejnerech.
// Takto vypadá definice selektoru, který lze uložit do databáze jako JSON interface IHtmlElementSelectorResult { selector: string; childNodeIndex: number; offset: number; } // Takto vypadá příklad serializované reprezentace Range objektu { start: { selector: "div > div > p:nth-of-type(4)", childNodeIndex: 0, offset: 10 }, end: { selector: "div > div > p:nth-of-type(8) > strong", childNodeIndex: 0, offset: 5 } }
Z takto serializované hodnoty, lze velmi snad zrekonstruovat Range objekt a nad ním poté provést označení.
Dokud nezmutuje DOM. Pokud zmutuje, tak není možná rekonstrukce. A právě tím, že chceme označit text, tak DOM mutuje (vkládáme <span> HTML elementy, které zvýrazňují text).
Takže znova a jinak.
Počtvrté
Opět jsem se vyskytl na zelené louce.
S kolegou jsme se nad tímto problémem zamysleli z jiného pohledu. Označení nebude prováděno na základě znalosti (reprezentace) DOMu, ale na základě znalosti počtu výskytu jednotlivých slov, které jsme vybrali, že chceme označit a které se „lámali“ skrze DOM.
Pustil jsem se do implementace. Procházením DOMU a sbíráním počtu výskytu označených slov, jsem došel k cíli. Fungovalo to jak na desktopu, tak i na mobilních aplikacích, je to relativně svižné – PC maximálně malé stovky milisekund, i při označení obrovského kusu textu (DOMu), na mobilních aplikacích podobně.
Využil jsem k procházení DOMu objekt TreeWalker, který jsem si nastavil tak, aby procházel jen ten označený kus textu a spočítal si výskyty slov. Výpočet jsem si uložil do JSON, který ač je závislý na velikosti textu a počtu výskytu slov, tak funguje skvěle.
TL;DR
Pro označení, uložení pozice a opětovné označení textu v HTML (DOM) nejsou vhodné způsoby:
- Vlastní implementace způsoby jako „počet znaků od začátku body elementu“, „počet znaků označeného textu“
- Použití knihovny rangy.js (nad kterou se v době psaní tohoto článku stejně už 2 roky nic neděje)
- Vlastní implementace způsobem serializace a deserializace Range objektu
Zatím jediný a funkční způsob byl vlastní implementace založená na znalosti označeného textu a počet výskytů jednotlivých slov, případně úseků slov – záleží na tom jak moc je DOM komplikovaný/velký/strukturovaný.
JavaScript: Metoda parseInt vrací nečekané výsledky
Metoda parseInt vrací celočíselnou hodnotu získanou z řetězce. Metoda toho umí ve skutečnosti více, než je od ní očekáváno – pokud řetězec začíná „0x“, považuje se řetězec za zápis čísla v šestnáckové soustavě, pokud začíná nulou, pak za zápis čísla v osmičkové soustavě. Druhým (nepovinným) parametrem můžeme metodě říct, v jaké číselné soustavě je hodnota uvedena. Pokud metoda při zpracování řetězce narazí na nepodporovaný symbol (znak), končí zpracování (což je v dokumentaci popsáno uvedeno jen v příkladu).
parseInt('abc') // vrací NaN parseInt('8abc') // vrací 8 - zpracování skončí u nepodporovaného symbolu "a" parseInt('0100') // vrací 64 - nula na začátku říká, že jde o osmičkovou soustavu, v osmičkové soustavě je "100" reprezentací hodnoty 64 parseInt('08') // vrací 0 - nula na začátku říká, že jde o osmičkovou soustavu, ta ale nepoužívá symbol 8, na kterém proto skončí zpracování parseInt('08', 10) // vrací 8 - uvedli jsme, že hodnota reprezentuje číslo v desítkové soustavě nepodporovanou mezerou parseInt('1 000', 10) // vrací 1 - zpracování skončí
Pozor proto na zpracování hodnot od uživatele. Pokud není jasné, jak uživatel hodnotu zadá (a to podle mě není jasné nikdy), je potřeba předat metodě parseInt i druhý parameter, jinak se dočkáte stejného překvapení jako já při zpracování času zadaného uživatelem: Vstup „07“ vrátí 7, vstup „08“ vrátí 0.
Pozor i na situaci, kdy uživatel zadá formátované číslo – například s oddělovačem tisíců. A takový javascript nemusíme ani psát ručně, stačí použít některý z validátorů (ASP.NET), který používá parseInt.
IE: Při použití auto-complete se nevyvolá událost onChange
Internet Explorer při použití auto-complete nevyvolá na daném prvku (inputu) událost onChange, bug (i když někteří by možná řekli by-design).
Mimo obskurnějších řešení se dá auto-complete na daném prvku prostě vypnout:
<input ... autocomplete="off" /> <asp:TextBox ... autocomplete="off" />
Případně ho vypnout na celém formuláři:
<form ... autocomplete="off">
…nefunguje bohužel možnost na celém formuláři vypnout a uvnitř na některých prvcích zapnout.
AJAX: Explicitní volání webových služeb z klienta
Microsoft ASP.NET AJAX 1.0 je právě ve stádiu Release Candidate (RC), nastává tedy správný čas pro bližší seznámení… ;-)
I když většinu potřebných AJAX features najdeme hotových ve formě server controlů, kdy nejsme nuceni napsat ani jediný řádek JavaScriptu, není od věci vědět, že i explicitní volání webových služeb je z ASP.NET AJAXu velmi jednoduché a snadno použitelné (obzvláště pokud poznáme, že je to prakticky stejné, jako přístup k webovým službám na straně serveru, resp. z .NET Frameworku).
Zpřístupnění webové služby
Základem každé AJAX-enabled stránky je control ScriptManager, který zajišťuje správu klientského AJAXového JavaScriptu. Zpřístupnění webové služby pak není nic jiného, než vytvoření ServiceReference na naší webovou službu
<asp:ScriptManager ID="ScriptManager" runat="server"> <Services> <asp:ServiceReference Path="~/MyWebService.asmx" /> </Services> </asp:ScriptManager>
Princip je to prakticky obdobný, jako v případě webových služeb na straně serveru (resp. v .NET aplikacích). Lze to přirovnat k přidání Web Reference do projektu ve Visual Studiu, nebo k použití utility wsdl.exe k vygenerování proxy třídy pro přístup k webové službě. Jediným zásadním rozdílem je, že lze takto přistupovat pouze na vlastní webové služby – tedy webové služby, které jsou součástí naší aplikace. Pokud bychom potřebovali přístup k externím webovým službám, není však problém vytvořit bridge prostřednictvím vlastní proxy webové služby.
Vraťme se však k našemu příkladu, dejme tomu, že webová služba ~/MyWebService.asmx by definovala jednu metodu
using System; using System.Data; using System.Web; using System.Collections; using System.Web.Services; using System.Web.Services.Protocols; using System.ComponentModel; using System.Web.Script.Services; namespace MyNamespace { [WebService(Namespace = "http://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] [ScriptService] public class MyWebService : System.Web.Services.WebService { [WebMethod] public string GetServerTime(string serverName) { return serverName + ": " + DateTime.Now.ToShortTimeString(); } } }
…jedná se o úplně klasickou webovou službu, která má navíc jen atribut [ScriptService] (jinak lze však tuto webovou službu využít jako kteroukoliv jinou). Atribut ScriptService zajišťuje, že nový handler .asmx souborů, s kterým ASP.NET AJAX přichází, dokáže při požadavku GET MyWebService.asmx/js vrátit vygenrovaný JavaScript s příslušnými proxy objekty pro práci s webovou službou. Ve skutečnosti totiž control ScriptManager přidá do stránky kód
</script src="MyWebService.asmx/js" type="text/javascript">
Pomocí atributu [ScriptMethod] u jednotlivých metod jsme navíc schopni řídit další detaily, např. formát, kterým spolu klientská a serverová část komunikují. Ve výchozí podobě totiž není komunikace na bázi XML, nýbrž v podobě JSON (JavaScript Object Notation), např.:
{"prefixText":"M","count":3} ["Michal", "Mirek", "Milan"]
Volání metod webové služby
Samotné volání metod webové služby je už hračkou, ASP.NET AJAX nám totiž vygeneruje objekt odpovídající webové službě, v namespace odpovídající webové službě a s metodami dle webové služby. Nejjednodušší volání tak může vypadat např.
<script type="text/javascript"> MyNamespace.MyWebService.GetServerTime('MyServer'); </script>
Musíme si však uvědomit, že veškeré AJAXové requesty jsou asynchronní, tedy musíme metodě říct, jakou funkci má zavolat po dokončení operace (přijetí odezvy od serveru) – výše uvedená nejjednodušší forma se tak moc často neuplatní. Nemáme-li totiž nastaven defaultní callback, pak nemáme jak převzít výsledek a jedná se prakticky jen o jednosměrnou komunikaci (navíc bez success-checkingu).
Další možností je tedy přidat jako další parametr metody funkci, která se má zavolat v okamžiku přijetí odezvy serveru, a která převezme a zpracuje výsledek. Parametrem funkce je výsledek volání metody přijatý v odezvě serveru.
<script type="text/javascript"> MyNamespace.MyWebService.GetServerTime('MyServer', OnComplete); function OnComplete(result) { alert(result); } </script>
Další vhodnou možností je přidat jako další parametr volání metody funkci, která se má zavolat v případě neúspěchu, v případě chyby:
<script type="text/javascript"> MyNamespace.MyWebService.GetServerTime('MyServer', OnComplete, OnError); function OnComplete(result, context) { alert(result); } function OnError(result) { alert(result.get_message()); } </script>
Jako poslední parametr lze přidat ještě určitý context, prostě obecná data, která se mají předat do metody OnComplete (neposílají se na server):
<script type="text/javascript"> MyNamespace.MyWebService.GetServerTime('MyServer', OnComplete, OnError, 'context'); function OnComplete(result, context) { alert(context); alert(result); } function OnError(result) { alert(result.get_message()); } </script>
Webové metody ve stránce
Abychom nemuseli vždy vytvářet úplnou webovou službu, máme též možnost jednoduché lokální metody umístit přímo do třídy samotné webové stránky (do kódu stránky), odekorovat je atributem [WebMethod] a následně k nim můžeme přistupovat prostřednictvím objektu PageMethods, aniž bychom museli jakokouliv webovou službu registrovat. Musíme jen povolit přepínač EnablePageMethods ScriptManageru:
public partial class _Default : System.Web.UI.Page { [System.Web.Services.WebMethod] public static string GetHelloWorld() { return "Hello world!"; } }
<script type="text/javascript"> PageMethods.GetHelloWorld(OnHWComplete); function OnHWComplete(result) { alert(result); } </script>
Webová metoda uvnitř stránky musí být statická!
Shrnutí
Explicitní volání webových služeb z klientské strany je velmi silnou zbraní ASP.NET AJAXu, pomocí které lze realizovat prakticky libovolnou AJAX funkčnost pro kterou nenajdeme vhodný hotový control. Pokud si navíc uvědomíme, že webová služba může pracovat prakticky s libovolnými serializovatelnými datovými typy (včetně DataSetu!), pak tímto dostáváme velmi příjemný nástroj pro klientské skriptování.
Související odkazy
IE7: Vlastní vyhledávání – Search Provider – OpenSearch
O možnosti přidání vlastního vyhledávání do Internet Exploreru 7 psal už dávno Michal Altair Valášek v článku „Jak přidat vyhledávání na stránkách do IE 7.0„, nebudu ho tu tedy opakovat a úvodní informace najde každý tam.
Doplňuji pouze, že existuje také možnost vytvoření linku, který daný search provider „nainstaluje“, resp. existuje JScript funkce AddSearchProvider, která IE řekne, že má nějaký search provider nainstalovat:
<a href="#" onclick="window.external.AddSearchProvider('/mySearch.xml')">My Search</a>
Další související články a odkazy:
onBeforeUnload – Potvrzovací dialog před odchodem ze stránky
V browseru, na stránkách, kde dochází k editaci záznamů, či jiné aktivitě, kterou je potřeba zakončit uložením či volbou nějakého tlačítka, se nám může hodit využít události onBeforeUnload k zobrazení potvrzovacího dialogu s dotazem, zde si uživatel opravdu přeje stránku opustit.
<html> <head> <script type="text/jscript"> // inicializace g_blnCheckUnload = true; function RunOnBeforeUnload() { if (g_blnCheckUnload) { window.event.returnValue = 'Text, který bude přidán do confirmačního dialogu'; } } function bypassCheck() { g_blnCheckUnload = false; } </script> </head> <body onBeforeUnload="RunOnBeforeUnload();"> <a href="http://www.havit.cz">dotaz zobrazen</a> <a href="http://www.havit.eu" onClick="bypassCheck">dotaz nezobrazen</a> </body> </html>
Událost onBeforeUnload se volá nejenom na odkazech a tlačítkách, ale i při zavírání okna prohlížeče a prakticky veškerých událostech, kde by mělo dojít k opuštění stránky.
Funguje to minimálně v Internet Exploreru a FireFoxu.
Modifikace s hlídáním změn
Nakonec se mi podařilo rozchodit i rozumnou podobu výše uvedeného, kdy je potvrzovací dotaz zobrazen jen při změně formulářových dat (a je tedy potřeba změny uložit):
<html> <head> <script type="text/jscript"> // inicializace g_blnCheckUnload = false; function RunOnBeforeUnload() { if (g_blnCheckUnload) { window.event.returnValue = 'Text, který bude přidán do confirmačního dialogu'; } } function bypassCheck() { g_blnCheckUnload = false; } function setupCheck() { g_blnCheckUnload = true; } registerEvents() { for (i = 0; i < document.forms[0].elements.length; i++) { document.forms[0].elements[i].onchange = setupCheck; } } </script> </head> <body onLoad="registerEvents();" onBeforeUnload="RunOnBeforeUnload();"> <form ...> <input .../> ... </form> <a href="http://www.havit.cz">dotaz zobrazen, jsou-li změny</a> <a href="http://www.havit.eu" onClick="bypassCheck">dotaz nezobrazen</a> </body> </html>
…další vylepšování je samozřejmě možné.
Update (PetrF): Pokud nějaké existující události onChange chceme zachovat
function registerEvents() { for (i = 0; i < document.forms[0].elements.length; i++) { var elem = document.forms[0].elements[i]; var fnOnChangeOld = (elem.onchange) ? elem.onchange : function () {}; elem.onchange = function () { fnOnChangeOld(); setupCheck() }; } }
…nebo přes jQuery.
Velikost okna browseru univerzálně
V jednotlivých prohlížečích se informace o aktuální velikosti okna zjišťuje dost různě, je tedy nutno použít určitou kaskádu pro „univerzální“ (alespoň trochu) vyhodnocení:
var winWidth, winHeight, d=document; if (typeof window.innerWidth!='undefined') { winWidth = window.innerWidth; winHeight = window.innerHeight; } else { if (d.documentElement && )typeof d.documentElement.clientWidth != 'undefined') && (d.documentElement.clientWidth != 0)) { winWidth = d.documentElement.clientWidth; winHeight = d.documentElement.clientHeight; } else { if (d.body && (typeof d.body.clientWidth != 'undefined')) { winWidth = d.body.clientWidth; winHeight = d.body.clientHeight; } } }
Mozilla: nefunguje window.navigate(‚url‘)
Mozilla navigate(‚url‘) nepodporuje.
Místo toho je potřeba použít standardní
window.location.href = 'url';
…což funguje i v IE.