Статья     Исходный код
CSS:
#taglist { margin: 1em; padding: 0.3em; width: 500px; height: 300px; border: dotted 1px #777; }
HTML:
<textarea id="taglist"> </textarea>
JavaScript:
function $(elid) { return document.getElementById(elid); } function bind(toObject, methodName) { return function(e){toObject[methodName](e)} } function listen(object, hevent, hfunc) { if (object.addEventListener) object.addEventListener(hevent,hfunc,false); else if (object.attachEvent) object.attachEvent('on'+hevent,hfunc); } function listenex(object, hevent, lobject, lfunc) { listen(object, hevent, bind(lobject, lfunc)); } function AutoTagsField(fieldid) { /* Properties */ this.field = $(fieldid); this.buffer = ""; //Handlers setupment listenex(this.field, "keyup", this, "key_up_handler"); /* Handlers */ //Handler this.key_up_handler = function() { //Getting values for `default` browsers var selectionStart = this.field.selectionStart; var selectionEnd = this.field.selectionEnd; //Creating selection method for our friend IE if (document.all) { //Creating range var range = document.selection.createRange(); //Cloning var crange = range.duplicate(); //Aquiring to the our field crange.moveToElementText(this.field); crange.setEndPoint("EndToEnd", range); //Overwriting values selectionStart = crange.text.length - range.text.length; selectionEnd = crange.text.length - selectionStart; } //Appending buffer information this.buffer += this.field.value.substring(selectionStart - 1, selectionStart); //Receiving matches var matches = this.filter_tags(this.field.value.match(/<.*?[^\/]>/gm)); //End of tag? if (this.buffer.match(/<\//g) && matches.length != 0) { //Tag name construction var tagname = "/" + matches.pop().match(/<(.*)>/)[1] + ">"; //Storing position var safe_position = selectionStart + tagname.length -1; //Appending to the right place this.field.value = this.field.value.substring(0, selectionStart-1) + tagname + this.field.value.substring(selectionStart); //Setting position this.field.selectionStart = safe_position; this.field.selectionEnd = safe_position; //IE selection if (document.all) { //Collapsing crange.collapse(true); //Moving selection start crange.moveStart("character", safe_position); //And selectio end crange.moveEnd("character", selectionEnd); //Performing crange.select(); } //Buffer should be empt now this.buffer = " "; //For our friend IE if (window.event) window.event.returnValue = false; //Oops? return false; } } /* Methods */ //Filtering tags this.filter_tags = function(tagarray) { //Exists? if (tagarray == null) return; //Debug /*var txt = "<pre>"; for (var l = 0; l < tagarray.length; l++) { txt += (l+1).toString() + ". " + tagarray[l].match(/<(.*?)(?:\s+[^>]+|)>/)[1] + "\r\n" }*/ //Creating arrays var clean = new Array(), result = new Array(); //Traversing for (i = 0, ilength = tagarray.length; i < ilength; i++) { //Oh, matching var name = tagarray[i].match(/<(?:\/|)(.*?)(?:\s+[^>]+|)>/)[1]; //Close tag? var is_close = false; if (tagarray[i].match(/<\/[^>]+>/)) is_close = true; //Adding to cleaning array if (clean[name] == null) { clean[name] = { "close" : 0, "open" : 0 }; } if (is_close) clean[name].close++; else clean[name].open++; } //Traversing again for (elem in clean) { //All is ok? if (clean[elem].open == clean[elem].close) continue; result.push("<" + elem + ">"); } //DEBUG /*txt += "\r\nCleaned:\r\n"; for (var l = 0; l < result.length; l++) { txt += (l+1).toString() + ". " + result[l].match(/<(?:\/|)(.*?)(?:\s+[^>]+|)>/)[1] + "\r\n" } txt += "</pre>"; $DEBUG(txt);*/ //To the user return result; } } var autotags = new AutoTagsField("taglist");