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ý.