Ukládání draftu komentářů

Školení, která pořádám

Taky vás dokáže naštvat, když delší dobu píšete komentář, dáváte si záležet na každé formulaci a pak se něco stane a o celý komentář přijdete? To něco může být chyba serveru, vaše nepozornost při zavírání oken nebo třeba agresivní WiFi, která jakýkoliv pokus o nahrání stránky přesměruje na svůj přihlašovací formulář.

Jako tvůrce aplikace tomu můžete zabránit ukládáním pracovní verze komentáře do nějakého úložiště. Komunikace se serverem po napsání každého znaku by byla příliš drahá, cookies nemají dostatečnou kapacitu a práce s nimi je nepohodlná, ale dá se použít úložiště localStorage. Je dostatečně velké (obvykle 5 MB na doménu) a ukládá se na klientovi, takže práce s ním je rychlá a nezabírá místo na serveru.

Kód je celkem přímočarý:

<textarea id="comment" name="comment" rows="20" cols="80"></textarea>
<script type="text/javascript">
if (window.localStorage) {
	var comment = document.getElementById('comment');
	var key = 'comment:' + location.pathname + location.search;
	comment.onkeyup = function () {
		localStorage.setItem(key, this.value);
	};
	if (!comment.value) {
		comment.value = localStorage.getItem(key);
	}
}
</script>

Kód funguje správně, pokud je na stránce jen jeden komentář. Pokud jich máme více, tak každému musíme přiřadit unikátní identifikátor, což ostatně můžeme udělat tak jako tak. URL stránky jsem zvolil jen v zájmu o obecnost a často to nemusí být nejlepší adept.

Ke kódu mám pár poznámek:

  1. Pokud uživatel vloží data ze schránky pomocí myši, tak se text neuloží, protože nedojde ke stisku klávesy. Pokud vám to vadí, můžete implementovat událost onpaste nebo data ukládat v pravidelném intervalu.
  2. Když si uživatel otevře tu stejnou stránku vícekrát, načte se mu do oken stejný obsah. Také se zachrání jen poslední text, který upravil. Nenapadá mě moc, jak by se to dalo řešit.
  3. Před nastavením komentáře se kontroluje, jestli je prázdný. To je proto, že některé prohlížeče za určitých ukolností zachovají obsah formulářů i po obnovení stránky (např. Firefox, pokud je povoleno ukládání stránky do keše). To řeší alespoň část předchozího bodu.

Zbývá smazání draftu po uložení finální verze. Mohlo by svádět to udělat v onsubmit, to by ale nepokrylo několik problematických scénářů. Skript, který provádí zápis do databáze, obvykle nic nevypisuje a jen provede přesměrování. Tam to tedy taky udělat nemůžeme. Můžeme si ale nastavit session proměnnou, kterou zkontrolujeme na stránce, kam uživatele přesměrujeme:

<?php
if (isset($_SESSION["comment_saved"])) {
	unset($_SESSION["comment_saved"]);
	?>
	<script type="text/javascript">
	if (window.localStorage) {
		var key = 'comment:' + location.pathname + location.search;
		localStorage.removeItem(key);
	}
	</script>
<?php } ?>

Klíč by bylo vhodné inicializovat na centrálním místě, v ukázce jsem to rozdělil jen kvůli pochopitelnosti.

Jakub Vrána, Řešení problému, 18.12.2013, diskuse: 34 (nové: 0)

Diskuse

Michal:

Jak tak koukám, funguje to i zde na webu. Nicméně se neukládá poslední znak. (Chrome Verze 31.0.1650.63 m)

ikona Jakub Vrána OpenID:

Díky za upozornění, opravil jsem to. Byla použitá starší verze kódu.

Michal:

U mě chyba stále přetrvává.

ikona Jakub Vrána OpenID:

Díky za vytrvalost. Starší verze kódu byla dokonce i v článku. Je potřeba použít událost onkeyup, nikoliv onkeypress.

Michal:

Již ok

Michal:

Stejně tak zůstává text ve storage, pokud jej vyberu a smažu naráz pomocí Backspace či Delete.

ikona Jakub Vrána OpenID:

Díky za upozornění, opravil jsem to. Byla použitá starší verze kódu.

ikona Jahoda:

Myslím, že by to chtělo, jak je napsáno v článku, rovnou něco více než onkeypress, hlídání klávesy Delete a podobně. Text je také třeba možné vyjmou do schránky, což se v současném stavu neprojeví (zůstane uložená varianta před vyjmutím).

Poznámku 2 může být zajímavé řešit synchronisací napříč stránkami. Potom by situace, kdy si několik stejných stránek bude navzájem přepisovat zálohu, neměla nastat.

ikona Jakub Vrána OpenID:

Synchronizace napříč stránkami je ve většině případů nežádoucí. Můžu si chtít otevřít tu stejnou stránku dvakrát a psát v nich dva různé komentáře (třeba reakce na dva různé příspěvky).

ikona Jahoda:

Potom by ale neměl být problém to zohlednit. A ukládat příspěvek do klíče „url-stranky-reakce-na-xxx“.

Jinak synchronisace se používá třeba na Diskusi JPW při focusu a bluru u <textarea>. Funguje to docela dobře.

Michal:

I tato chyba zůstává. Z cache to není.

ikona Jakub Vrána OpenID:

Díky, i tohle už by mělo fungovat.

Michal:

Taktéž v pořádku.

Jan Tojnar:

Ukládání stavu formuláře sice usnadní spoustě lidem práci, ale myslím si, že by se o to, stejně jako o změnu velikosti pole pomocí táhla, měl starat spíše prohlížeč. Tím se obejdou zmiňované problémy a na všech stránkách bude mít uživatel konzistentní prostředí.

Programátor by se měl zaměřit hlavně na možnost obnovení odeslání formuláře (mimo jiné i na předvyplnění polí v případě znovuodeslání požadavku s již neplatným CSRF tokenem), pokud dojde k selhání.

V současnosti funkci ukládání polí nabízí třeba rozšíření Lazarus (http://getlazarus.com/).

Jan Tojnar:

Nicméně dokud tato funkce nebude dostatečně rozšířena, tak proč ji neponechat.

Werkov:

Zcela souhlasím s prvním příspěvkem.

U toho druhého je to trochu filozofický/sociální problém. Pokud bude funkce ponechána, nebude to možná nutit vývojáře prohlížečů potažmo jejich uživatele mít tuto featuru v prohlížeči.

Hever:

Ve wbových aplikacích můžou být formuláře generované dynamicky/javascriptem, v tom případě prohlížeč pomoct nemůže a je potřeba, aby to řešila aplikace.

paranoiq:

více stejných formulářů na jedné URL by šlo řešit tak, že by se jako klíč použil celý stav formuláře krom samotného inputu (např. když ve skrytém inputu je id příspěvku, ke kterému komentář píšu)

ikona Jan Štráfelda:

Pěkný příspěvek. Zrovna jsem si nedávno všiml, že text zapsaný  ve vlákně BaseCampu zůstane, i když zavřete prohlížeč a vrátíte se jindy. Říkal jsem si, že to asi řeší přes cookies, localStorage mě nenapadlo. Článek mi ušetřil dlouhé zkoumání, děkuji :-)

O:

Neni lepsi to proste ukladat kazdych 10s? Pak nam odpadaji uplne vsechny problemy s udalostma, onkeypress, on paste atd atd.

ikona Jakub Vrána OpenID:

Já to nemám rád z několika důvodů:

1. Při ladění občas používám funkci zastavení při dalším spuštění kódu (umí to Firebug). Hodí se to třeba k ladění toho, jaká událost se vyvolá. Časovače tohle dost znepříjemňují.

2. Někdy si text napíšu vedle, zkopíruji a do prohlížeče vložím najednou. Když se něco pokazí, než se text stihne uložit, tak jsem přišel o celý text.

3. Někdy třeba opravím překlep uvnitř komentáře. Kontrolovat, jestli se tahle změna už uložila nebo ještě ne, je dost otrava.

O:

1. True, asynchroni veci se spatne debuguji.
2. True, ale v tom pripade to mas ulozeny vedle. Nejde o to mit uplne posledni pismenko, kdyz ti spadne prohlizec. Ale o to zachranit co se da. 10s textu kazdy dopise znova.
3. Nic se nekontroluje, proste kazdych 10s ulozis cely text v komentari, je to akce klienta, server se nezatezuje (aspon tvuj pripad, jinak by se to pres ajax asi posilalo na server)

Podle me tento zpusob dela i google a vubec skoro vsude, v podstate sem ten tvuj zpusob nikde snad ani nevidel. Jak je to v FB?

ikona Jakub Vrána OpenID:


2. Jde o to, že vedle už to můžu mít zavřené.
3. Jako autor: Dopíšu komentář, opravím překlepy, spadne to. Příště se to nahraje znova, ale není jasné, které opravy se ještě stihly uložit a které ne.

Třeba Google Keep ukládá data okamžitě, ne až po timeoutu. FB to pokud vím neukládá vůbec.

Visitor:

Obě metody by šly zkombinovat. Ukládat na onKeyUp a zároveň co 5 sekund (pro ostatní případy).

Michal Sänger:

Ještě by šlo použit SessionStorage

ikona Jakub Vrána OpenID:

To se smaže, když se zavře prohlížeč nebo panel.

myšnekočklokanutrie:

Díky za článek. Už jsem se s tím někde setkal (memá to náhodou Wikipedie?).

Já osobně bych volil ukládání po určitém časovém intervalu a nejspíš bych i hlídal přepisování stejného obsahu - myslím, že by to bylo méně přesunů dat a řešilo by to i to vložení myší.

Ad otevření ve více oknech) šlo by to vyřešit následujícně - php náhodně vygeneruje ID HTML elementu (místo "comment" např. "comment77864", a zapamatuje si to) a do Javascriptu se vloží kód ad hoc pro toto ID (pro použití v samostatném souboru by se mohl kód napsat jako funkce a předávat id jako parametr).

ikona Jakub Vrána OpenID:

Když otvírám novou stránku a něco je uložené: Jak poznám, že chci psát nový komentář nebo obnovit ten starý protože něco spadlo?

Když něco spadlo a komentářů mám uložených víc: Který obnovit?

Bylo by potřeba k tomu udělat uživatelské rozhraní.

NoxArt:

To mi právě přijde jako dobrá volba, mít roletku a možnost vybrat si, co obnovit. Něco v tom smyslu má myslim Word a zdá se, že i výše zmiňovaný Lazarus.

Taky by to řešilo bod #3, tam je problém, že jsme mohli naopak nějaké pole vymazat a tudíž by v zapamatovaných byla špatná informace. Pokud je polí hodně, uživatel si nemusí všimnout (tedy neřeším momentálně komentáře s jediným polem, ale obecně formuláře).

ikona Marek Vavrečan:

čo takto použiť storage events na tú synchronizáciu medzi tabmi?
http://dev.w3.org/html5/webstorage/#the-storage-event

ikona Marek Vavrečan:

a ten paste by sa dal vyriešiť cez input event. Takže takto aj so synchronizáciou medzi tabmi a s paste udalosťou:

<textarea id="comment" name="comment" rows="20" cols="80"></textarea>
<script type="text/javascript">
if (window.localStorage) {
    var comment = document.getElementById('comment');
    var key = 'comment:' + location.pathname + location.search;

    if (!comment.value) {
        comment.value = localStorage.getItem(key);
    }

    window.addEventListener('storage', function(e) {
        if (e.key == key)
            comment.value = e.newValue;
    }, false);

    window.addEventListener('input',function(e){
         if (e.srcElement == comment)
            localStorage.setItem(key, e.srcElement.value);
    }, false);
}
</script>

ikona Lex Vjatkin:

Na http://flowreader.com úspěšně používáme. Až na "synchronizaci" mezi tabama to hlídá veškerý vstup. Takže za mě LocalStorage +1.

Hever:

Rozlišit formuláře více oken/tabů - k tomu mě napadá tak jedině to párovat s nějakým unikátním identifikátorem v url za křížkem # (location.hash).

myšnekočklokanutrie:

To může být problém. Třeba jQuery UI tabs hash mění - a to je jen jeden příklad z mnoha.

Vložit komentář

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2018 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.