Ticket #960: remote_autocomplete_v3.diff
| File remote_autocomplete_v3.diff, 25.4 kB (added by madrobby, 3 years ago) |
|---|
-
actionpack/lib/action_view/helpers/javascripts/prototype.js
old new 452 452 element.style.backgroundColor = "#ffff" + current.toColorPart(); 453 453 } 454 454 } 455 456 Effect.Fade = Class.create(); 457 Effect.Fade.prototype = { 458 initialize: function(element) { 459 this.element = $(element); 460 this.start = 100; 461 this.finish = 0; 462 this.current = this.start; 463 this.fade(); 464 }, 465 466 fade: function() { 467 if (this.isFinished()) return; 468 if (this.timer) clearTimeout(this.timer); 469 this.setOpacity(this.element, this.current); 470 this.current -= 10; 471 this.timer = setTimeout(this.fade.bind(this), 100); 472 }, 473 474 isFinished: function() { 475 return this.current <= this.finish; 476 }, 477 478 setOpacity: function(element, opacity) { 479 opacity = (opacity == 100) ? 99.999 : opacity; 480 element.style.filter = "alpha(opacity:"+opacity+")"; 481 element.style.opacity = opacity/100; 482 } 483 } 484 485 /*--------------------------------------------------------------------------*/ 486 487 Event = { 488 KEY_BACKSPACE: 8, 489 KEY_TAB: 9, 490 KEY_RETURN: 13, 491 KEY_ESC: 27, 492 KEY_LEFT: 37, 493 KEY_UP: 38, 494 KEY_RIGHT: 39, 495 KEY_DOWN: 40, 496 KEY_DELETE: 46, 497 498 element: function(event) { 499 return event.srcElement || event.currentTarget; 500 }, 501 502 stop: function(event) { 503 if(event.preventDefault) 504 { event.preventDefault(); event.stopPropagation(); } 505 else 506 event.returnValue = false; 507 }, 508 509 getParentNodeOrSelfByName: function(event, nodeName) { 510 element = Event.element(event); 511 while(element.nodeName != nodeName && element.parentNode) 512 element = element.parentNode; 513 return element; 514 }, 515 516 observeKeypress: function(element, observer) { 517 if(navigator.appVersion.indexOf('AppleWebKit')>0) 518 { $(element).addEventListener("keydown",observer,false); return; } 519 if($(element).addEventListener) $(element).addEventListener("keypress",observer,false) 520 else if($(element).attachEvent) $(element).attachEvent("onkeydown",observer); 521 }, 522 523 observeBlur: function(element, observer) { 524 if($(element).addEventListener) $(element).addEventListener("blur",observer,false) 525 else if($(element).attachEvent) $(element).attachEvent("onblur",observer); 526 }, 527 528 observeClick: function(element, observer) { 529 if($(element).addEventListener) $(element).addEventListener("click",observer,false) 530 else if($(element).attachEvent) $(element).attachEvent("onclick",observer); 531 }, 532 533 observeHover: function(element, observer) { 534 if($(element).addEventListener) $(element).addEventListener("mouseover",observer,false) 535 else if($(element).attachEvent) $(element).attachEvent("onmouseover",observer); 536 } 537 538 } 539 540 Text = { 541 stripTags: function(htmlstr) { 542 return htmlstr.replace(/<\/?[^>]+>/gi,""); 543 }, 544 decodeHTML: function(htmlstr) { 545 return htmlstr.replace(/</gi,"<").replace(/>/gi,">").replace(/"/gi,'"').replace(/'/gi,"'").replace(/&/gi,"&").replace(/[\n\r]/gi,""); 546 } 547 } 548 549 Element = { 550 show: function(element) { 551 $(element).style.display = ''; 552 }, 553 hide: function(element) { 554 $(element).style.display = 'none'; 555 }, 556 samePositionAs: function(element, aselement) { 557 if(navigator.appVersion.indexOf('MSIE')>0) { 558 $(element).style.top = $(aselement).style.top; 559 $(element).style.left = $(aselement).style.left; 560 $(element).style.width = $(aselement).offsetWidth; 561 $(element).style.height = $(aselement).offsetHeight; 562 } 563 } 564 } 565 566 Ajax.Autocomplete = Class.create(); 567 Ajax.Autocomplete.prototype = (new Ajax.Base()).extend({ 568 initialize: function(element, update, url, options) { 569 this.element = $(element); 570 this.update = $(update); 571 this.has_focus = false; 572 this.changed = false; 573 this.active = false; 574 this.index = 0; 575 this.entry_count = 0; 576 this.url = url; 577 578 this.setOptions(options); 579 this.options.asynchronous = true; 580 this.options.onComplete = this.onComplete.bind(this) 581 this.options.frequency = this.options.frequency || 0.4; 582 this.options.min_chars = this.options.min_chars || 1; 583 this.options.method = 'post'; 584 585 if(this.options.indicator) 586 this.indicator = $(this.options.indicator); 587 588 this.observer = null; 589 590 Event.observeKeypress (this.element, this.onKeyPress.bindAsEventListener(this)); 591 Event.observeClick (document, this.onBlur.bindAsEventListener(this)); 592 }, 593 594 show: function() { 595 Element.show(this.update); 596 if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0)) { 597 new Insertion.Before(this.update, 598 '<iframe id="' + this.update.id + '_iefix" style="display:none;" src="javascript:false;" frameborder="0" scrolling="no"></iframe>'); 599 this.iefix = $(this.update.id+'_iefix'); 600 this.iefix.style.position = 'absolute'; 601 this.iefix.style.zIndex = 1; 602 this.update.style.zIndex = 2; 603 } 604 if(this.iefix) { 605 Element.samePositionAs(this.iefix, this.update); 606 Element.show(this.iefix); 607 } 608 }, 609 610 hide: function() { 611 if(this.iefix) Element.hide(this.iefix); 612 Element.hide(this.update); 613 }, 614 615 startIndicator: function() { 616 if(this.indicator) Element.show(this.indicator); 617 }, 618 619 stopIndicator: function() { 620 if(this.indicator) Element.hide(this.indicator); 621 }, 622 623 onObserverEvent: function() { 624 this.changed = false; 625 if(this.element.value.length>=this.options.min_chars) { 626 this.startIndicator(); 627 this.options.parameters = this.options.callback ? 628 this.options.callback(this.element, Form.Element.getValue(this.element)) : 629 Form.Element.getValue(this.element); 630 new Ajax.Request(this.url, this.options); 631 } else { 632 this.active = false; 633 this.hide(); 634 } 635 }, 636 637 onComplete: function(request) { 638 if(!this.changed) { 639 this.update.innerHTML = request.responseText; 640 641 if(this.update.firstChild && this.update.firstChild.childNodes) { 642 this.entry_count = 643 this.update.firstChild.childNodes.length; 644 for (var i = 0; i < this.entry_count; i++) { 645 entry = this.get_entry(i); 646 entry.autocompleteIndex = i; 647 Event.observeHover(entry, this.onHover.bindAsEventListener(this)); 648 Event.observeClick(entry, this.onClick.bindAsEventListener(this)); 649 } 650 } else { 651 this.entry_count = 0; 652 } 653 654 this.stopIndicator(); 655 656 this.index = 0; 657 this.render(); 658 } 659 }, 660 661 onKeyPress: function(event) { 662 if(this.active) 663 switch(event.keyCode) { 664 case Event.KEY_TAB: 665 case Event.KEY_RETURN: 666 this.select_entry(); 667 Event.stop(event); 668 case Event.KEY_ESC: 669 this.hide(); 670 this.active = false; 671 return; 672 case Event.KEY_LEFT: 673 case Event.KEY_RIGHT: 674 return; 675 case Event.KEY_UP: 676 this.mark_previous(); 677 this.render(); 678 return; 679 case Event.KEY_DOWN: 680 this.mark_next(); 681 this.render(); 682 return; 683 } 684 else 685 if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) 686 return; 687 688 this.changed = true; 689 this.has_focus = true; 690 691 if(this.observer) clearTimeout(this.observer); 692 this.observer = 693 setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); 694 }, 695 696 onHover: function(event) { 697 element = Event.getParentNodeOrSelfByName(event, 'LI'); 698 if(this.index != element.autocompleteIndex) 699 { 700 this.index = element.autocompleteIndex; 701 this.render(); 702 } 703 }, 704 705 onClick: function(event) { 706 element = Event.getParentNodeOrSelfByName(event, 'LI'); 707 this.index = element.autocompleteIndex; 708 this.select_entry(); 709 }, 710 711 onBlur: function(event) { 712 element = Event.element(event); 713 if(element==this.update) return; 714 while(element.parentNode) 715 { element = element.parentNode; if(element==this.update) return; } 716 this.hide(); 717 this.active = false; 718 }, 719 720 render: function() { 721 if(this.entry_count > 0) { 722 for (var i = 0; i < this.entry_count; i++) 723 this.get_entry(i).className = 724 this.index==i ? 'selected' : ''; 725 726 if(this.has_focus) { 727 if(this.get_current_entry().scrollIntoView) 728 this.get_current_entry().scrollIntoView(false); 729 730 this.show(); 731 this.active = true; 732 } 733 } else this.hide(); 734 }, 735 736 mark_previous: function() { 737 if(this.index > 0) this.index-- 738 else this.index = this.entry_count-1; 739 }, 740 741 mark_next: function() { 742 if(this.index < this.entry_count-1) this.index++ 743 else this.index = 0; 744 }, 745 746 get_entry: function(index) { 747 return this.update.firstChild.childNodes[index]; 748 }, 749 750 get_current_entry: function() { 751 return this.get_entry(this.index); 752 }, 753 754 select_entry: function() { 755 this.hide(); 756 this.active = false; 757 value = Text.decodeHTML(Text.stripTags(this.get_current_entry().innerHTML)); 758 this.element.value = value; 759 this.element.focus(); 760 } 761 }); -
actionpack/lib/action_view/helpers/form_helper.rb
old new 1 1 require 'cgi' 2 2 require File.dirname(__FILE__) + '/date_helper' 3 3 require File.dirname(__FILE__) + '/tag_helper' 4 require File.dirname(__FILE__) + '/javascript_helper' 5 require File.dirname(__FILE__) + '/url_helper' 4 6 5 7 module ActionView 6 8 module Helpers … … 141 143 142 144 class InstanceTag #:nodoc: 143 145 include Helpers::TagHelper 146 include Helpers::JavascriptHelper 147 include Helpers::UrlHelper 144 148 145 149 attr_reader :method_name, :object_name 146 150 … … 151 155 def initialize(object_name, method_name, template_object, local_binding = nil) 152 156 @object_name, @method_name = object_name, method_name 153 157 @template_object, @local_binding = template_object, local_binding 158 @controller = @template_object.controller # url_for in :remote_autocomplete 154 159 if @object_name.sub!(/\[\]$/,"") 155 160 @auto_index = @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}").id_before_type_cast 156 161 end … … 166 171 options["type"] = field_type 167 172 options["value"] ||= value_before_type_cast unless field_type == "file" 168 173 add_default_name_and_id(options) 169 tag("input", options) 174 autocomplete_text = "" 175 if options["remote_autocomplete"] 176 if options["indicator"] 177 autocomplete_text << 178 tag("img", { :id => "#{options['id']}_autocomplete_indicator", :style => 'display:none;', :src => options["indicator"] }) 179 options.delete("indicator") 180 end 181 autocomplete_text << # content_tag needed to have <div></div> instead of <div/> 182 content_tag("div", "", { :id => "#{options['id']}_autocomplete", :class => "autocomplete", :style => "display:none;" }) 183 autocomplete_text << 184 autocomplete_for(options['id'], 185 { :url => options["remote_autocomplete"], :with => "'for=' + escape(value)", :indicator => "#{options['id']}_autocomplete_indicator" }) 186 options["autocomplete"] = "off" 187 options.delete("remote_autocomplete") 188 end 189 tag_text = tag("input", options) 190 tag_text << autocomplete_text 191 return tag_text 170 192 end 171 193 172 194 def to_radio_button_tag(tag_value, options = {}) -
actionpack/lib/action_view/helpers/javascript_helper.rb
old new 147 147 def observe_form(form_id, options = {}) 148 148 build_observer('Form.Observer', form_id, options) 149 149 end 150 151 # Adds Ajax autocomplete functionality to the text input field with the 152 # DOM ID specified by +field_id+. 153 # 154 # This function expects that the called action returns a HTML <ul> list, 155 # or nothing if no entries should be displayed for autocompletion. 156 # Note: There must be no whitespace between the returned elements. 157 # 158 # Required +options+ are: 159 # <tt>:url</tt>:: Specifies the DOM ID of the element whose 160 # innerHTML should be updated with the autocomplete 161 # entries returned by XMLHttpRequest. 162 # 163 # Addtional +options+ are: 164 # <tt>:update</tt>:: Specifies the DOM ID of the element whose 165 # innerHTML should be updated with the autocomplete 166 # entries returned by the Ajax request. 167 # Defaults to field_id + '_autocomplete' 168 # <tt>:with</tt>:: A Javascript expression specifying the 169 # parameters for the XMLHttpRequest. This defaults 170 # to 'value', which in the evaluated context 171 # refers to the new field value. 172 # <tt>:indicator</tt>:: Specifies the DOM ID of an elment which will be 173 # displayed while autocomplete is running. 174 # 175 def autocomplete_for(field_id, options = {}) 176 function = "new Ajax.Autocomplete(" 177 function << "'#{field_id}', " 178 function << "'" + (options[:update] || "#{field_id}_autocomplete") + "', " 179 function << "'#{url_for(options[:url])}'" 180 js_options = {} 181 js_options[:callback] = "function(element, value) {return #{options[:with]}}" if options[:with] 182 js_options[:indicator] = "'#{options[:indicator]}'" if options[:indicator] 183 function << (', {' + js_options.map {|k, v| "#{k}:#{v}"}.join(', ') + '}') 184 function << ")" 185 186 "<script type=\"text/javascript\">#{function}</script>" 187 end 188 189 # Use this method in your view to generate a return for the Ajax automplete requests. 190 # 191 # Example Action: 192 # @items = Item.find_all([ 'LOWER(description) LIKE ?', 193 # '%' + @params["for"].downcase + '%' ], 'description ASC') 194 # render_without_layout 195 # 196 # Example View: 197 # <%= autocomplete_responder @items, 'description' %> 198 # 199 def autocomplete_responder(entries, field, phrase = nil) 200 "<ul>#{entries.map { |entry| '<li>' + (phrase ? highlight(entry[field],phrase) : h(entry[field])) + '</li>' }.join}</ul>" if entries 201 end 150 202 151 203 # Escape carrier returns and single and double quotes for Javascript segments. 152 204 def escape_javascript(javascript) -
railties/html/javascripts/prototype.js
old new 452 452 element.style.backgroundColor = "#ffff" + current.toColorPart(); 453 453 } 454 454 } 455 456 Effect.Fade = Class.create(); 457 Effect.Fade.prototype = { 458 initialize: function(element) { 459 this.element = $(element); 460 this.start = 100; 461 this.finish = 0; 462 this.current = this.start; 463 this.fade(); 464 }, 465 466 fade: function() { 467 if (this.isFinished()) return; 468 if (this.timer) clearTimeout(this.timer); 469 this.setOpacity(this.element, this.current); 470 this.current -= 10; 471 this.timer = setTimeout(this.fade.bind(this), 100); 472 }, 473 474 isFinished: function() { 475 return this.current <= this.finish; 476 }, 477 478 setOpacity: function(element, opacity) { 479 opacity = (opacity == 100) ? 99.999 : opacity; 480 element.style.filter = "alpha(opacity:"+opacity+")"; 481 element.style.opacity = opacity/100; 482 } 483 } 484 485 /*--------------------------------------------------------------------------*/ 486 487 Event = { 488 KEY_BACKSPACE: 8, 489 KEY_TAB: 9, 490 KEY_RETURN: 13, 491 KEY_ESC: 27, 492 KEY_LEFT: 37, 493 KEY_UP: 38, 494 KEY_RIGHT: 39, 495 KEY_DOWN: 40, 496 KEY_DELETE: 46, 497 498 element: function(event) { 499 return event.srcElement || event.currentTarget; 500 }, 501 502 stop: function(event) { 503 if(event.preventDefault) 504 { event.preventDefault(); event.stopPropagation(); } 505 else 506 event.returnValue = false; 507 }, 508 509 getParentNodeOrSelfByName: function(event, nodeName) { 510 element = Event.element(event); 511 while(element.nodeName != nodeName && element.parentNode) 512 element = element.parentNode; 513 return element; 514 }, 515 516 observeKeypress: function(element, observer) { 517 if(navigator.appVersion.indexOf('AppleWebKit')>0) 518 { $(element).addEventListener("keydown",observer,false); return; } 519 if($(element).addEventListener) $(element).addEventListener("keypress",observer,false) 520 else if($(element).attachEvent) $(element).attachEvent("onkeydown",observer); 521 }, 522 523 observeBlur: function(element, observer) { 524 if($(element).addEventListener) $(element).addEventListener("blur",observer,false) 525 else if($(element).attachEvent) $(element).attachEvent("onblur",observer); 526 }, 527 528 observeClick: function(element, observer) { 529 if($(element).addEventListener) $(element).addEventListener("click",observer,false) 530 else if($(element).attachEvent) $(element).attachEvent("onclick",observer); 531 }, 532 533 observeHover: function(element, observer) { 534 if($(element).addEventListener) $(element).addEventListener("mouseover",observer,false) 535 else if($(element).attachEvent) $(element).attachEvent("onmouseover",observer); 536 } 537 538 } 539 540 Text = { 541 stripTags: function(htmlstr) { 542 return htmlstr.replace(/<\/?[^>]+>/gi,""); 543 }, 544 decodeHTML: function(htmlstr) { 545 return htmlstr.replace(/</gi,"<").replace(/>/gi,">").replace(/"/gi,'"').replace(/'/gi,"'").replace(/&/gi,"&").replace(/[\n\r]/gi,""); 546 } 547 } 548 549 Element = { 550 show: function(element) { 551 $(element).style.display = ''; 552 }, 553 hide: function(element) { 554 $(element).style.display = 'none'; 555 }, 556 samePositionAs: function(element, aselement) { 557 if(navigator.appVersion.indexOf('MSIE')>0) { 558 $(element).style.top = $(aselement).style.top; 559 $(element).style.left = $(aselement).style.left; 560 $(element).style.width = $(aselement).offsetWidth; 561 $(element).style.height = $(aselement).offsetHeight; 562 } 563 } 564 } 565 566 Ajax.Autocomplete = Class.create(); 567 Ajax.Autocomplete.prototype = (new Ajax.Base()).extend({ 568 initialize: function(element, update, url, options) { 569 this.element = $(element); 570 this.update = $(update); 571 this.has_focus = false; 572 this.changed = false; 573 this.active = false; 574 this.index = 0; 575 this.entry_count = 0; 576 this.url = url; 577 578 this.setOptions(options); 579 this.options.asynchronous = true; 580 this.options.onComplete = this.onComplete.bind(this) 581 this.options.frequency = this.options.frequency || 0.4; 582 this.options.min_chars = this.options.min_chars || 1; 583 this.options.method = 'post'; 584 585 if(this.options.indicator) 586 this.indicator = $(this.options.indicator); 587 588 this.observer = null; 589 590 Event.observeKeypress (this.element, this.onKeyPress.bindAsEventListener(this)); 591 Event.observeClick (document, this.onBlur.bindAsEventListener(this)); 592 }, 593 594 show: function() { 595 Element.show(this.update); 596 if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0)) { 597 new Insertion.Before(this.update, 598 '<iframe id="' + this.update.id + '_iefix" style="display:none;" src="javascript:false;" frameborder="0" scrolling="no"></iframe>'); 599 this.iefix = $(this.update.id+'_iefix'); 600 this.iefix.style.position = 'absolute'; 601 this.iefix.style.zIndex = 1; 602 this.update.style.zIndex = 2; 603 } 604 if(this.iefix) { 605 Element.samePositionAs(this.iefix, this.update); 606 Element.show(this.iefix); 607 } 608 }, 609 610 hide: function() { 611 if(this.iefix) Element.hide(this.iefix); 612 Element.hide(this.update); 613 }, 614 615 startIndicator: function() { 616 if(this.indicator) Element.show(this.indicator); 617 }, 618 619 stopIndicator: function() { 620 if(this.indicator) Element.hide(this.indicator); 621 }, 622 623 onObserverEvent: function() { 624 this.changed = false; 625 if(this.element.value.length>=this.options.min_chars) { 626 this.startIndicator(); 627 this.options.parameters = this.options.callback ? 628 this.options.callback(this.element, Form.Element.getValue(this.element)) : 629 Form.Element.getValue(this.element); 630 new Ajax.Request(this.url, this.options); 631 } else { 632 this.active = false; 633 this.hide(); 634 } 635 }, 636 637 onComplete: function(request) { 638 if(!this.changed) { 639 this.update.innerHTML = request.responseText; 640 641 if(this.update.firstChild && this.update.firstChild.childNodes) { 642 this.entry_count = 643 this.update.firstChild.childNodes.length; 644 for (var i = 0; i < this.entry_count; i++) { 645 entry = this.get_entry(i); 646 entry.autocompleteIndex = i; 647 Event.observeHover(entry, this.onHover.bindAsEventListener(this)); 648 Event.observeClick(entry, this.onClick.bindAsEventListener(this)); 649 } 650 } else { 651 this.entry_count = 0; 652 } 653 654 this.stopIndicator(); 655 656 this.index = 0; 657 this.render(); 658 } 659 }, 660 661 onKeyPress: function(event) { 662 if(this.active) 663 switch(event.keyCode) { 664 case Event.KEY_TAB: 665 case Event.KEY_RETURN: 666 this.select_entry(); 667 Event.stop(event); 668 case Event.KEY_ESC: 669 this.hide(); 670 this.active = false; 671 return; 672 case Event.KEY_LEFT: 673 case Event.KEY_RIGHT: 674 return; 675 case Event.KEY_UP: 676 this.mark_previous(); 677 this.render(); 678 return; 679 case Event.KEY_DOWN: 680 this.mark_next(); 681 this.render(); 682 return; 683 } 684 else 685 if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) 686 return; 687 688 this.changed = true; 689 this.has_focus = true; 690 691 if(this.observer) clearTimeout(this.observer); 692 this.observer = 693 setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); 694 }, 695 696 onHover: function(event) { 697 element = Event.getParentNodeOrSelfByName(event, 'LI'); 698 if(this.index != element.autocompleteIndex) 699 { 700 this.index = element.autocompleteIndex; 701 this.render(); 702 } 703 }, 704 705 onClick: function(event) { 706 element = Event.getParentNodeOrSelfByName(event, 'LI'); 707 this.index = element.autocompleteIndex; 708 this.select_entry(); 709 }, 710 711 onBlur: function(event) { 712 element = Event.element(event); 713 if(element==this.update) return; 714 while(element.parentNode) 715 { element = element.parentNode; if(element==this.update) return; } 716 this.hide(); 717 this.active = false; 718 }, 719 720 render: function() { 721 if(this.entry_count > 0) { 722 for (var i = 0; i < this.entry_count; i++) 723 this.get_entry(i).className = 724 this.index==i ? 'selected' : ''; 725 726 if(this.has_focus) { 727 if(this.get_current_entry().scrollIntoView) 728 this.get_current_entry().scrollIntoView(false); 729 730 this.show(); 731 this.active = true; 732 } 733 } else this.hide(); 734 }, 735 736 mark_previous: function() { 737 if(this.index > 0) this.index-- 738 else this.index = this.entry_count-1; 739 }, 740 741 mark_next: function() { 742 if(this.index < this.entry_count-1) this.index++ 743 else this.index = 0; 744 }, 745 746 get_entry: function(index) { 747 return this.update.firstChild.childNodes[index]; 748 }, 749 750 get_current_entry: function() { 751 return this.get_entry(this.index); 752 }, 753 754 select_entry: function() { 755 this.hide(); 756 this.active = false; 757 value = Text.decodeHTML(Text.stripTags(this.get_current_entry().innerHTML)); 758 this.element.value = value; 759 this.element.focus(); 760 } 761 }); -
railties/lib/rails_generator/generators/components/scaffold/templates/style.css
old new 16 16 a:visited { color: #666; } 17 17 a:hover { color: #fff; background-color:#000; } 18 18 19 .autocomplete { 20 position:absolute; 21 z-index:10; 22 border:1px solid #aaa; 23 background-color:#fff; 24 } 25 .autocomplete ul, .autocomplete li { margin:0; padding:0; border: 0; } 26 .autocomplete li { padding:2px; display:block; cursor:pointer; } 27 .autocomplete .selected { background-color: #ffa; } 28 19 29 .fieldWithErrors { 20 30 padding: 2px; 21 31 background-color: red;