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.
  1. <html>  
  2.     <head>  
  3.         <script type="text/javascript">  
  4.             // Constants  
  5.             var CONST_IDENT = '    ';  
  6.             var CONST_NL = "\n";  
  7.   
  8.             var CONST_CHARCODE_S = 115;  
  9.             var CONST_KEYCODE_S = 83;  
  10.             var CONST_KEYCODE_TAB = 9;  
  11.             var CONST_KEYCODE_ENTER = 13;  
  12.   
  13.             var timer = null;  
  14.   
  15.   
  16.             // Add a handler to an event  
  17.             // TODO: http://simon.incutio.com/archive/2004/05/26/addLoadEvent  
  18.             // TODO: http://dean.edwards.name/weblog/2005/09/busted/  
  19.             function addEventHandler(obj, eventType, handler) {  
  20.                 if (obj.addEventListener) {  
  21.                     obj.addEventListener(eventType, handler, true);  
  22.                     return true;  
  23.                 } else if (obj.attachEvent) {  
  24.                     var r = obj.attachEvent("on"+eventType, handler);  
  25.                     return r;  
  26.                 } else {  
  27.                     return false;  
  28.                 }  
  29.             }  
  30.   
  31.   
  32.             // Setting up textareas on the page  
  33.             addEventHandler(window, 'load', setupControls);  
  34.   
  35.   
  36.             // Assign the key <-> action mapping handler function to the textareas  
  37.             function setupControls() {  
  38.                 var elems = document.getElementsByTagName('TEXTAREA');  
  39.                 for(i = 0; i < elems.length; i++) {  
  40.                     addEventHandler(elems<span style="font-style:italic">, 'keypress', function(event) {  
  41.                         return controlsKeyHandler(this, event);  
  42.                     });  
  43.                     // For Internet Explorer  
  44.                     // In this browser the onkeypress event doesn't fire on special keys  
  45.                     // so we need to find an alternate way to handle them.  
  46.                     if (document.selection) {  
  47.                         addEventHandler(elems[i], 'keydown', function(event) {  
  48.                             return controlsKeyHandler(this, event);  
  49.                         });  
  50.                     }  
  51.                 }  
  52.             }  
  53.   
  54.   
  55.             // A helper function for debugging purpose.  
  56.             function showObj( obj, colNum ) {  
  57.                 var alertStr = '';  
  58.                 var j = 1;  
  59.                 for( var i in obj ) {  
  60.                     switch( i ) {  
  61.                         case 'outerHTML' : continue;  
  62.                         case 'innerHTML' : continue;  
  63.                     }  
  64.                     alertStr += ( '' + i + '->' + obj[i] + "\t" );  
  65.                     if ( j++ % colNum == 0 ) {  
  66.                         alertStr += "\n";  
  67.                     }  
  68.                 }  
  69.                 alert( alertStr );  
  70.             }  
  71.   
  72.   
  73.             // Key <-> action mapping handler function  
  74.             // onkeydown parameter shows if the event handler is called by onkeydown  
  75.             function controlsKeyHandler(textField, event) {  
  76.                 var eventevent = event ? event : (window.event ? window.event : null);  
  77.                 if (event) {  
  78.                     var textField = event.target ? event.target : (event.srcElement ? event.srcElement : null);  
  79.                     if (textField) {  
  80.                         var keyCode = event.charCode ? event.charCode : (event.keyCode ? event.keyCode : event.which);  
  81.                         if (event.modifiers) {  
  82.                             var alt = event.modifiers & Event.ALT_MASK;  
  83.                             var ctrl = event.modifiers & Event.CONTROL_MASK;  
  84.                             var shift = event.modifiers & Event.SCHIFT_MASK;  
  85.                             var meta = event.modifiers & Event.META_MASK;  
  86.                         } else {  
  87.                             var alt = event.altKey;  
  88.                             var ctrl = event.ctrlKey;  
  89.                             var shift = event.shiftKey;  
  90.                             var meta = false;  
  91.                         }  
  92.                         // Some helper variables  
  93.                         var modifiers = alt || ctrl || shift || meta;  
  94.                         var onlyCtrl = ctrl && !(alt || shift || meta);  
  95.                         var onlyAlt =  alt && !(ctrl || shift || meta);  
  96.                         var onlyShift = shift && !(alt || ctrl || meta);  
  97.                         var onlyMeta = meta && !(alt || shift || ctrl);  
  98.                         var onkeydown = event.type.toLowerCase() == "keydown";  
  99.                         var onkeypress = event.type.toLowerCase() == "keypress";  
  100.   
  101.   
  102.                         debug("|"+event.type+":"+keyCode+"-m:"+modifiers+"-c:"+onlyCtrl+"-a:"+onlyAlt+"-s:"+onlyShift);  
  103.   
  104.   
  105.                         // This flag controls if default action should be canceled  
  106.                         var stopPropagation = true;  
  107.   
  108.                         // Key <-> action mappings  
  109.                         if (onlyCtrl && ((onkeypress && keyCode == CONST_CHARCODE_S) || (onkeydown && keyCode == CONST_KEYCODE_S))) {  
  110.                             controlsSaveTemplate();  
  111.                         } else if (keyCode == CONST_KEYCODE_TAB) {  
  112.                             controlsAddIdent(textField);  
  113.                         } else if (keyCode == CONST_KEYCODE_ENTER) {  
  114.                             controlsKeepIdent(textField);  
  115.                         } else {  
  116.                             stopPropagation = false;  
  117.                         }  
  118.   
  119.                         if (stopPropagation) {  
  120.                             if (event.returnValue) {  
  121.                                 event.returnValue = false;  
  122.                                 event.cancelBubble = true;  
  123.                             } else if (event.preventDefault) {  
  124.                                 event.preventDefault();  
  125.                                 event.stopPropagation()  
  126.                             } else {  
  127.                                 return false;  
  128.                             }  
  129.                         }  
  130.                     }  
  131.                 }  
  132.             }  
  133.   
  134.   
  135.             function debug(msg)  
  136.             {  
  137.                 var debug = document.getElementById('debug');  
  138.                 if (debug) {  
  139.                     debug.value = msg+"\n"+debug.value;  
  140.                     if (timer) {  
  141.                         clearInterval(timer);  
  142.                     }  
  143.                     timer = setInterval(function() {debug.value = "\n"+debug.value; clearInterval(timer);}, 1000);  
  144.                 }  
  145.             }  
  146.   
  147.   
  148.             // Call the save button's click() function  
  149.             function controlsSaveTemplate(textField)  
  150.             {  
  151.                 alert("save");  
  152.             }  
  153.   
  154.   
  155.             // Add ident string on pressing TAB  
  156.             function controlsAddIdent(textField)  
  157.             {  
  158.                 insertAtCursor(textField, CONST_IDENT);  
  159.             }  
  160.   
  161.   
  162.             // Keeps the previous line's ident on pressing enter  
  163.             function controlsKeepIdent(textField)  
  164.             {  
  165.                  var ident = '';  
  166.                  var position = getCursorPosition(textField, true);  
  167.   
  168.                  var i = position-1;  
  169.                  while(i > -1 && textField.value.charAt(i) != CONST_NL) {  
  170.                      i--;  
  171.                  }  
  172.   
  173.                  var currentLine = textField.value.substring(i+1, position);  
  174.                  if (currentLine) {  
  175.                      var match = currentLine.match(/^([\s]+)[\S]+/);  
  176.                      if (match) {  
  177.                          ident = match[1];  
  178.                      }  
  179.                  }  
  180.   
  181.                  insertAtCursor(textField, CONST_NL+ident);  
  182.             }  
  183.   
  184.   
  185.             // Insert text before and after the current cursor position  
  186.             function insertAtCursor(textField, before, after) {  
  187.                 if (after == null) {  
  188.                     after = '';  
  189.                 }  
  190.                 if (textField.createTextRange) {  
  191.                     textField.focus();  
  192.                     var position = getCursorPosition(textField);  
  193.   
  194.                     // Adding the before/after text to the textRange  
  195.                     var range = document.selection.createRange();  
  196.                     range.text = before+after;  
  197.   
  198.                     // Moving the cursor to the right position  
  199.                     if (before.length) {  
  200.                         position += before.length;  
  201.                     }  
  202.                     setCursorPosition(textField, position);  
  203.                 } else if (textField.selectionStart ||textField.selectionStart == 0) {  
  204.                     var scrollTop = textField.scrollTop;  
  205.                     var startPos = textField.selectionStart;  
  206.                     var endPos = textField.selectionEnd;  
  207.                     textFieldtextField.value = textField.value.substring(0, startPos)  
  208.                                     + (before + after)  
  209.                                     + textField.value.substring(endPos, textField.value.length);  
  210.                     setCursorPosition(textField, startPos+before.length, scrollTop);  
  211.                 } else {  
  212.                     var position = textField.value.length+before.length;  
  213.                     textField.value +=  (before + after);  
  214.                     setCursorPosition(textField, position);  
  215.                 }  
  216.             }  
  217.   
  218.   
  219.             // Get the current cursor position of a text range  
  220.             function getCursorPosition(textField, MSIEReturnNotLogicalPosition)  
  221.             {  
  222.                 // Handling this issue in case of Internet Explorer need a little hack.  
  223.                 // The problem is, that IE handle the concept of new line different  
  224.                 // from text field value and textRange "character" unit point of view.  
  225.                 // In the value the \r,\n charcters exist separately, but when use  
  226.                 // the textRange.move("charcter", 1) new line is only one step.  
  227.                 // That's why we need a little correction.  
  228.                 if (textField.createTextRange) {  
  229.                     var newLineNumAfterCursor = 0;  
  230.                     match = textField.value.match(/\n/g);  
  231.                     if (match) {  
  232.                         newLineNumAfterCursor = match.length;  
  233.                     }  
  234.   
  235.                     textField.focus();  
  236.                     var position = textField.value.length;  
  237.                     var cursor = document.selection.createRange().duplicate();  
  238.   
  239.                     while (cursor.parentElement() == textField && cursor.move('character', 1)) {  
  240.                         if (textField.value.charAt(position - 1) == CONST_NL) {  
  241.                             position -1;  
  242.                             newLineNumAfterCursor--;  
  243.                         }  
  244.                         position--;  
  245.                     }  
  246.   
  247.                     if (MSIEReturnNotLogicalPosition) {  
  248.                         return position + 1;  
  249.                     } else {  
  250.                         return position + 1 - (newLineNumAfterCursor);  
  251.                     }  
  252.                 } else if (textField.selectionStart || textField.selectionStart == 0) {  
  253.                     textField.focus();  
  254.                     return textField.selectionStart;  
  255.                 }  
  256.             }  
  257.   
  258.   
  259.             // Set the current cursor position of a text range  
  260.             function setCursorPosition(textField, position, scrollTop)  
  261.             {  
  262.                 if (textField.createTextRange) {  
  263.                     var textRange = textField.createTextRange();  
  264.                     textRange.moveStart("character", position);  
  265.                     textRange.collapse();  
  266.                     textRange.select();  
  267.                 } else if (textField.selectionStart) {  
  268.                     textField.focus();  
  269.                     textFieldtextField.selectionStart = textField.selectionEnd = position;  
  270.                 }  
  271.                 if (typeof scrollTop != "undefined" && (textField.scrollTop || textField.scrollTop == 0)) {  
  272.                     textField.scrollTop = scrollTop;  
  273.                 }  
  274.             }  
  275.         </script>  
  276.     </head>  
  277.     <body onload="setCursorPosition(document.getElementById('foo'), 522);">  
  278.         <a href="#" onmouseover="alert(getCursorPosition(document.getElementById('foo'))); return false;">foo</a>  
  279.         <a href="#" onmouseover="alert(document.getElementById('foo').scrollTop); return false;">bar</a>  
  280.         <form>  
  281.             <textarea name="foo" id="foo" rows="12" cols="50">  
  282.                 1111111111 2222222222 3333333333 4444444444 5555555555  
  283.             </textarea>  
  284.             <textarea id="debug" rows="40" cols="70"></textarea>  
  285.             <input type="submit">  
  286.         </form>  
  287.     </body>  
  288. </html></span>  
É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!