ugrás a tartalomhoz

Keresztplatformos turbó textarea a Jézuskától

Hodicska Gergely · 2005. Dec. 24. (Szo), 16.02
Szeretnék a karácsonykor is minket olvasóknak egy kis meglepetéssel szolgálni, aminek némileg érdekes története van. Egy levelező listás szállal indult, melynek keretén belül szerettem volna egy minél inkább böngésző független módszert találni arra, hogy egy textarea belső területén le tudjam kérdezni illetve be tudjam állítani a kurzor pozícióját, valamint bővíteni tudjam annak szerkesztő funkcióit. Sajnos ezekben a napokban idő hiányában esélyem sem volt, hogy utána járjak a témának. De tegnap jött a váratlan fordulat.

Úgy alakult, hogy Bukarestben karácsonyozom, és épp még egy kis elvarratlan munkának álltam volna neki, de sajnos az itteni kábelnet szolgáltató ezt nem így akarta, másfél napig nem volt internet kapcsolatom. Első bosszúság utáni elővettem ezt a témát, és szerencsére offline anyagokból, illetve a levelezőlistás válaszok alapján sikerült egy működő megoldást találni.

Célom részben az volt, hogy két oldal betöltődés között egy szövegmezőben a kurzor megőrizze a pozícióját, másrészt hogy egy olyan keretet alakítsak ki, amely megfelelő alapot nyújt egy szövegmezőben való szerkesztési funkciók kellő kibővítéséhez. Most ebből két apró, de hasznos funkciót mutatnék be, melyek nagyban segítik programkódok, sablonok szövegmezőben történő szerkesztését: tab leütésekor egy előre beállított behúzást helyez el a szövegben, illetve enter leütésekor automatikusan beszúrja az előző sorból örökölt behúzást. Persze könnyen belátható, hogy innetől kezdve a lehetőségek tárháza végtelen. Több-kevesebb munkával kedvenc editorunk alap funkcióit kedvünk szerint megvalósíthatjuk.

Tekintettel a hátralévő ajándék csomagolási feladataimra, a részletek magyarázása nélkül lentebb megtalálható a forráskód, illetve kipróbálható működés közben is. Így is sok hasznos dolgot kinézhetünk belőle az eseménykezelők diszkrét csatolásától az esemény objektum elérésén át a karakter kódok (módosítók) megszerzéséig és a cél elem megállapításáig.

Alapvetően nem tűnt bonyolultnak a dolog, csak sajnos az a böngésző, mely folyamatosan karban tartja kreativitásunkat, és nem hagy minket ellustulni, ezúttal is feladatokkal látott el. A trükk a getCursorPosition függvényben található. Úgy tűnik van egy kis probléma a karakter fogalmával a textarea-ban tárolt szöveg és a belőle létrehozott textRange objektum szempontjából. Az előbbi az új sort két karakternek veszi, míg ez utóbbi csak egynek, és ebből adódnak a problémák.

A lenti megoldást Internet Explorer 6.0 és Firefox 1.5 alatt teszteltem, de elvileg minden Range objektumot illetve textarea.selectionStart tulajdonságot támogató böngészőben működhet.

Internet Explorer alatt van még egy kis gondom, a tab lenyomásakor úgy tűnik, hogy nem hívódik meg az eseménykezelő függvény, ez még megoldásra vár.
Update: ez már kiküszöbölve, az alábbi módon.

Első körben megpróbáltam onkeydown eseményre tenni az eseménykezelőt, de Firefox esetén ilyenkor nem működött az események alapértelmezett kezelésének kikpacsolása. Erre vonatkozóan nem találtam a fejlesztői dokumentációban információt, tehát ez megítélésem szerint hibának tekinthető.

Második körben arra gondoltam, hogy akkor Internet Explorer esetén az onkeydown eseményhez is hozzárendelem ugyanezt a kezelőt, az event.type tulajdonságból úgyis megtudható, hogy mi okozta a függvény meghívását.

Persze a törpök élete nem csak játék és mese. Ugyanis amikor megnyomunk egy billentyűt, akkor két dologra is kíváncsiak lehetünk: milyen karakter érkezik ennek hatására, illetve melyik billentyűt ütötték le. Ezek megkülönböztetésére a szabvány szerint két külön tulajdonság szolgál: előbbit az event objektum charCode, utóbbit a charKey tulajdonságából olvashatjuk ki. Karitatív böngészőnkben viszont csak charKey van, és az a logikája, hogy onkeydown, onkeyup esetén a billentyű, onkeypress esetében pedig a karakter kódját tartalmazza.

Hogy egy kicsit tisztázódjanak számomra is a dolgok, tettem egy kis hibaellenőrző ablakot a kódba, és ezzel frissítettem a példa oldalt is. Ebből láthatjuk, hogy pontosan milyen eseménykezelő hívódik meg, milyen sorrendben, milyen módosító van lenyomva. Ebből például egyből kiderült egy érdekesség: Internet Explorer alatt onkeypress esemény esetén is a billentyű kódját kapjuk vissza, ha például shift is le van nyomva. Tettem egy kis példát a kódba Ctrl+S kezelésére is.
<html>
	<head>
        <script type="text/javascript">
            // Constants
            var CONST_IDENT = '    ';
            var CONST_NL = "\n";

            var CONST_CHARCODE_S = 115;
            var CONST_KEYCODE_S = 83;
            var CONST_KEYCODE_TAB = 9;
            var CONST_KEYCODE_ENTER = 13;

            var timer = null;


            // Add a handler to an event
            // TODO: http://simon.incutio.com/archive/2004/05/26/addLoadEvent
            // TODO: http://dean.edwards.name/weblog/2005/09/busted/
            function addEventHandler(obj, eventType, handler) {
                if (obj.addEventListener) {
                    obj.addEventListener(eventType, handler, true);
                    return true;
                } else if (obj.attachEvent) {
                    var r = obj.attachEvent("on"+eventType, handler);
                    return r;
                } else {
                    return false;
                }
            }


            // Setting up textareas on the page
            addEventHandler(window, 'load', setupControls);


            // Assign the key <-> action mapping handler function to the textareas
            function setupControls() {
                var elems = document.getElementsByTagName('TEXTAREA');
                for(i = 0; i < elems.length; i++) {
                    addEventHandler(elems, 'keypress', function(event) {
                        return controlsKeyHandler(this, event);
                    });
                    // For Internet Explorer
                    // In this browser the onkeypress event doesn't fire on special keys
                    // so we need to find an alternate way to handle them.
                    if (document.selection) {
                        addEventHandler(elems[i], 'keydown', function(event) {
                            return controlsKeyHandler(this, event);
                        });
                    }
                }
            }


            // A helper function for debugging purpose.
            function showObj( obj, colNum ) {
                var alertStr = '';
                var j = 1;
                for( var i in obj ) {
                    switch( i ) {
                        case 'outerHTML' : continue;
                        case 'innerHTML' : continue;
                    }
                    alertStr += ( '' + i + '->' + obj[i] + "\t" );
                    if ( j++ % colNum == 0 ) {
                        alertStr += "\n";
                    }
                }
                alert( alertStr );
            }


            // Key <-> action mapping handler function
            // onkeydown parameter shows if the event handler is called by onkeydown
            function controlsKeyHandler(textField, event) {
                var event = event ? event : (window.event ? window.event : null);
                if (event) {
                    var textField = event.target ? event.target : (event.srcElement ? event.srcElement : null);
                    if (textField) {
                        var keyCode = event.charCode ? event.charCode : (event.keyCode ? event.keyCode : event.which);
                        if (event.modifiers) {
                            var alt = event.modifiers & Event.ALT_MASK;
                            var ctrl = event.modifiers & Event.CONTROL_MASK;
                            var shift = event.modifiers & Event.SCHIFT_MASK;
                            var meta = event.modifiers & Event.META_MASK;
                        } else {
                            var alt = event.altKey;
                            var ctrl = event.ctrlKey;
                            var shift = event.shiftKey;
                            var meta = false;
                        }
                        // Some helper variables
                        var modifiers = alt || ctrl || shift || meta;
                        var onlyCtrl = ctrl && !(alt || shift || meta);
                        var onlyAlt =  alt && !(ctrl || shift || meta);
                        var onlyShift = shift && !(alt || ctrl || meta);
                        var onlyMeta = meta && !(alt || shift || ctrl);
                        var onkeydown = event.type.toLowerCase() == "keydown";
                        var onkeypress = event.type.toLowerCase() == "keypress";


                        debug("|"+event.type+":"+keyCode+"-m:"+modifiers+"-c:"+onlyCtrl+"-a:"+onlyAlt+"-s:"+onlyShift);


                        // This flag controls if default action should be canceled
                        var stopPropagation = true;

                        // Key <-> action mappings
                        if (onlyCtrl && ((onkeypress && keyCode == CONST_CHARCODE_S) || (onkeydown && keyCode == CONST_KEYCODE_S))) {
                            controlsSaveTemplate();
                        } else if (keyCode == CONST_KEYCODE_TAB) {
                            controlsAddIdent(textField);
                        } else if (keyCode == CONST_KEYCODE_ENTER) {
                            controlsKeepIdent(textField);
                        } else {
                            stopPropagation = false;
                        }

                        if (stopPropagation) {
                            if (event.returnValue) {
                                event.returnValue = false;
                                event.cancelBubble = true;
                            } else if (event.preventDefault) {
                                event.preventDefault();
                                event.stopPropagation()
                            } else {
                                return false;
                            }
                        }
                    }
                }
            }


            function debug(msg)
            {
                var debug = document.getElementById('debug');
                if (debug) {
                    debug.value = msg+"\n"+debug.value;
                    if (timer) {
                        clearInterval(timer);
                    }
                    timer = setInterval(function() {debug.value = "\n"+debug.value; clearInterval(timer);}, 1000);
                }
            }


            // Call the save button's click() function
            function controlsSaveTemplate(textField)
            {
                alert("save");
            }


            // Add ident string on pressing TAB
            function controlsAddIdent(textField)
            {
                insertAtCursor(textField, CONST_IDENT);
            }


            // Keeps the previous line's ident on pressing enter
            function controlsKeepIdent(textField)
            {
                 var ident = '';
                 var position = getCursorPosition(textField, true);

                 var i = position-1;
                 while(i > -1 && textField.value.charAt(i) != CONST_NL) {
                     i--;
                 }

                 var currentLine = textField.value.substring(i+1, position);
                 if (currentLine) {
                     var match = currentLine.match(/^([\s]+)[\S]+/);
                     if (match) {
                         ident = match[1];
                     }
                 }

                 insertAtCursor(textField, CONST_NL+ident);
            }


            // Insert text before and after the current cursor position
            function insertAtCursor(textField, before, after) {
                if (after == null) {
                    after = '';
                }
                if (textField.createTextRange) {
                    textField.focus();
                    var position = getCursorPosition(textField);

                    // Adding the before/after text to the textRange
                    var range = document.selection.createRange();
                    range.text = before+after;

                    // Moving the cursor to the right position
                    if (before.length) {
                        position += before.length;
                    }
                    setCursorPosition(textField, position);
                } else if (textField.selectionStart ||textField.selectionStart == 0) {
                    var scrollTop = textField.scrollTop;
                    var startPos = textField.selectionStart;
                    var endPos = textField.selectionEnd;
                    textField.value = textField.value.substring(0, startPos)
                                    + (before + after)
                                    + textField.value.substring(endPos, textField.value.length);
                    setCursorPosition(textField, startPos+before.length, scrollTop);
                } else {
                    var position = textField.value.length+before.length;
                    textField.value +=  (before + after);
                    setCursorPosition(textField, position);
                }
            }


            // Get the current cursor position of a text range
            function getCursorPosition(textField, MSIEReturnNotLogicalPosition)
            {
                // Handling this issue in case of Internet Explorer need a little hack.
                // The problem is, that IE handle the concept of new line different
                // from text field value and textRange "character" unit point of view.
                // In the value the \r,\n charcters exist separately, but when use
                // the textRange.move("charcter", 1) new line is only one step.
                // That's why we need a little correction.
                if (textField.createTextRange) {
                    var newLineNumAfterCursor = 0;
                    match = textField.value.match(/\n/g);
                    if (match) {
                        newLineNumAfterCursor = match.length;
                    }

                    textField.focus();
                    var position = textField.value.length;
                    var cursor = document.selection.createRange().duplicate();

                    while (cursor.parentElement() == textField && cursor.move('character', 1)) {
                        if (textField.value.charAt(position - 1) == CONST_NL) {
                            position -= 1;
                            newLineNumAfterCursor--;
                        }
                        position--;
                    }

                    if (MSIEReturnNotLogicalPosition) {
                        return position + 1;
                    } else {
                        return position + 1 - (newLineNumAfterCursor);
                    }
                } else if (textField.selectionStart || textField.selectionStart == 0) {
                    textField.focus();
                    return textField.selectionStart;
                }
            }


            // Set the current cursor position of a text range
            function setCursorPosition(textField, position, scrollTop)
            {
                if (textField.createTextRange) {
                    var textRange = textField.createTextRange();
                    textRange.moveStart("character", position);
                    textRange.collapse();
                    textRange.select();
                } else if (textField.selectionStart) {
                    textField.focus();
                    textField.selectionStart = textField.selectionEnd = position;
                }
                if (typeof scrollTop != "undefined" && (textField.scrollTop || textField.scrollTop == 0)) {
                    textField.scrollTop = scrollTop;
                }
            }
		</script>
	</head>
	<body onload="setCursorPosition(document.getElementById('foo'), 522);">
		<a href="#" onmouseover="alert(getCursorPosition(document.getElementById('foo'))); return false;">foo</a>
		<a href="#" onmouseover="alert(document.getElementById('foo').scrollTop); return false;">bar</a>
		<form>
			<textarea name="foo" id="foo" rows="12" cols="50">
                1111111111 2222222222 3333333333 4444444444 5555555555
            </textarea>
            <textarea id="debug" rows="40" cols="70"></textarea>
			<input type="submit">
		</form>
	</body>
</html>
És végül szerintem milyen a jó Karácsony? Rég nem látott rokonokkal találkozós, sírós nevetős, töltött káposztából jól beevős, boldogságos örülős!

Szerintem ilyet kívánok Nektek! :)

[i]Update: ez a thread kapcsolódik a témához.
 
1

Nálunk is töltött káposztva van ! :)

tiny · 2005. Dec. 24. (Szo), 17.31
Köszi, azt hiszem ennek még sokan hasznát fogják venni. Köztük én is. :)
Mr.Tiny
2

<Nincs cím>

Anonymous · 2005. Dec. 24. (Szo), 18.25
Hát bízzunk benne, hogy az internetszolgáltatók még megtréfálnak párszor :D
Így lehetőséged lesz burjánzó kreativitásod termékét megosztani velünk!
3

új sor

gerzson · 2005. Dec. 25. (V), 09.23
Az előbbi az új sort két karakternek veszi, míg ez utóbbi csak egynek, és ebből adódnak a problémák. Ez a \r\n vs \n?

testing can reveal the presence of errors, but never their absence. - Edsger Dijkstra
4

inkább "fizikai" vs "logikai"

Hodicska Gergely · 2005. Dec. 25. (V), 09.41
Nem hiszem, hiszen egy rendszeren belül vagyunk, ugyanazzal a szöveggel dolgozik a textRange is. Inkább azt tudom elkézpzelni, hogy a move metódusa használatakor "character" mértékegység esetén egynek veszi ezt a kettőt, mint ahogy amikor lépkedsz a szövegben, ott is csak egyszer kell a nyilat megnyomnod, amikor a sorvégére érsz.


Felhő
5

update

Hodicska Gergely · 2005. Dec. 25. (V), 11.29
click


Felhő
6

kis bugfix

Hodicska Gergely · 2005. Dec. 26. (H), 00.11
Firefox alatt, ha a textarea tartalma lejebb volt görgetve, akkor szöveg beszúrása esetén felugrott. Ez javítva lett a scrollTop megfelelő beállításával, illetve annyiban bővült az egész cucc, hogy a setCursorPosition függvénynek most már ezt paraméterként is meg lehet adni, így például kedvünkre beállíthatjuk betöltődés után is.


Felhő
7

include() probléma

erenon · 2006. Már. 21. (K), 22.20
Először is köszönetet mondok a cikk írójának, az ötletek 1-1-ig szuperek, több részét is felhasználtam.
De egy nagy problémám akadt:
Van egy oldal, amiben benne van a textarea, a megfelelő JS kódokkal. Önállóan tökéletesen működik, viszont ha egy másik fájl ágyazza be include()-parancscsal, hibát jelez. (text is not definied)(a text a textarea neve) vagypedig (ha a textet definiálom az elején) textField has no properties.
Nagyon kezdő vagyok JS-ben, lehet, h hatalmas elvi hibát vétettem, de önerőből nem tudom kiküszöbölni.
Segítségeteket előre is köszönöm!
8

include?

Hodicska Gergely · 2006. Már. 22. (Sze), 00.04
Szia!


Mit szeretnél pontosan az include paranccsal elérni? Ez arra való, hogy egy PHP fájlba behúzz egy másik PHP fájlt, nem pedig, hogy JS kódot illessz az oldalba.

Meg amúgy is ilyen esetekben szerencsés, ha példakódot is mutatsz, anélkül elég nehéz látatatlanban bármit is mondani.


Felhő
9

JS a PHP-ben

erenon · 2006. Már. 22. (Sze), 00.23
A javascript az includolt PHP-ben van. (pl.: textarea.php) A textarea.php magában tökéletesen fut, viszont egy másik oldalba beágyazva betegeskedik. Nincs külső JS-fájl, tehát nem az eléréssel van a baj.
10

kód nélkül esélytelen

Hodicska Gergely · 2006. Már. 22. (Sze), 01.02
Így látatlanban esélytelen bármit is mondani. Sorry. Állj neki debuggolni, amiben az egyik legfontosabb, hogy mindig lépésről lépésre ellenőrizd, hogy azok a feltételezések, amelyeket a programod egyes részeivel, változóival szemben vélsz, azok ténylegesen úgy vannak. Tapasztalataim szerint a legtöbben ott hibáznak egy probléma felderítésében, hogy nem ellenőriznek lépésről lépésre minden lehetséges hiba forrást, így nem derül ki, hogy a hiba nem is ott van adott esetben, ahol keresik.


Felhő
11

Újra

erenon · 2006. Már. 22. (Sze), 15.20
Rendben, köszönöm. Debuggolás megvolt, kitörlöm és újrakezdem őgyis elég csúnya lett a kód így összeeszkábálva.
És még1x köszönet érte, mert sok jó ötletet egyesít újrafelhasználhatóan!