Ticket #960: remote_autocomplete.diff
| File remote_autocomplete.diff, 47.4 kB (added by madrobby, 4 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 /*--------------------------------------------------------------------------*/ 457 458 Event = { 459 KEY_BACKSPACE: 8, 460 KEY_TAB: 9, 461 KEY_RETURN: 13, 462 KEY_ESC: 27, 463 KEY_LEFT: 37, 464 KEY_UP: 38, 465 KEY_RIGHT: 39, 466 KEY_DOWN: 40, 467 KEY_DELETE: 46, 468 469 element: function(event) { 470 return event.srcElement || event.currentTarget; 471 }, 472 473 stop: function(event) { 474 if(event.preventDefault) 475 { event.preventDefault(); event.stopPropagation(); } 476 else 477 event.returnValue = false; 478 }, 479 480 getParentNodeOrSelfByName: function(event, nodeName) { 481 element = Event.element(event); 482 while(element.nodeName != nodeName && element.parentNode) 483 element = element.parentNode; 484 return element; 485 }, 486 487 observeKeypress: function(element, observer) { 488 if(navigator.appVersion.indexOf('AppleWebKit')>0) 489 { $(element).addEventListener("keydown",observer,false); return; } 490 if($(element).addEventListener) $(element).addEventListener("keypress",observer,false) 491 else if($(element).attachEvent) $(element).attachEvent("onkeydown",observer); 492 }, 493 494 observeBlur: function(element, observer) { 495 if($(element).addEventListener) $(element).addEventListener("blur",observer,false) 496 else if($(element).attachEvent) $(element).attachEvent("onblur",observer); 497 }, 498 499 observeClick: function(element, observer) { 500 if($(element).addEventListener) $(element).addEventListener("click",observer,false) 501 else if($(element).attachEvent) $(element).attachEvent("onclick",observer); 502 }, 503 504 observeHover: function(element, observer) { 505 if($(element).addEventListener) $(element).addEventListener("mouseover",observer,false) 506 else if($(element).attachEvent) $(element).attachEvent("onmouseover",observer); 507 } 508 509 } 510 511 Text = { 512 stripTags: function(htmlstr) { 513 return htmlstr.replace(/<\/?[^>]+>/gi,""); 514 }, 515 decodeHTML: function(htmlstr) { 516 return htmlstr.replace("<","<").replace(">",">").replace(""",'"').replace("&","&"); 517 } 518 } 519 520 Element = { 521 show: function(element) { 522 $(element).style.display = ''; 523 }, 524 hide: function(element) { 525 $(element).style.display = 'none'; 526 }, 527 samePositionAs: function(element, aselement) { 528 if(navigator.appVersion.indexOf('MSIE')>0) { 529 $(element).style.top = $(aselement).style.top; 530 $(element).style.left = $(aselement).style.left; 531 $(element).style.width = $(aselement).offsetWidth; 532 $(element).style.height = $(aselement).offsetHeight; 533 } 534 } 535 } 536 537 Ajax.Autocomplete = Class.create(); 538 Ajax.Autocomplete.prototype = (new Ajax.Base()).extend({ 539 initialize: function(element, update, url, options) { 540 this.element = $(element); 541 this.update = $(update); 542 this.has_focus = false; 543 this.changed = false; 544 this.active = false; 545 this.index = 0; 546 this.entry_count = 0; 547 this.url = url; 548 549 this.setOptions(options); 550 this.options.asynchronous = true; 551 this.options.onComplete = this.onComplete.bind(this) 552 this.options.frequency = this.options.frequency || 0.4; 553 this.options.min_chars = this.options.min_chars || 1; 554 this.options.method = 'post'; 555 556 if(this.options.indicator) 557 this.indicator = $(this.options.indicator); 558 559 this.observer = null; 560 561 Event.observeKeypress (this.element, this.onKeyPress.bindAsEventListener(this)); 562 Event.observeClick (document, this.onBlur.bindAsEventListener(this)); 563 }, 564 565 show: function() { 566 Element.show(this.update); 567 if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0)) { 568 new Insertion.Before(this.update, 569 '<iframe id="' + this.update.id + '_iefix" style="display:none;" src="javascript:false;" frameborder="0" scrolling="no"></iframe>'); 570 this.iefix = $(this.update.id+'_iefix'); 571 this.iefix.style.position = 'absolute'; 572 this.iefix.style.zIndex = 1; 573 this.update.style.zIndex = 2; 574 } 575 if(this.iefix) { 576 Element.samePositionAs(this.iefix, this.update); 577 Element.show(this.iefix); 578 } 579 }, 580 581 hide: function() { 582 if(this.iefix) Element.hide(this.iefix); 583 Element.hide(this.update); 584 }, 585 586 startIndicator: function() { 587 if(this.indicator) Element.show(this.indicator); 588 }, 589 590 stopIndicator: function() { 591 if(this.indicator) Element.hide(this.indicator); 592 }, 593 594 onObserverEvent: function() { 595 this.changed = false; 596 if(this.element.value.length>=this.options.min_chars) { 597 this.startIndicator(); 598 this.options.parameters = this.options.callback ? 599 this.options.callback(this.element, Form.Element.getValue(this.element)) : 600 Form.Element.getValue(this.element); 601 new Ajax.Request(this.url, this.options); 602 } else { 603 this.active = false; 604 this.hide(); 605 } 606 }, 607 608 onComplete: function(request) { 609 if(!this.changed) { 610 this.update.innerHTML = request.responseText; 611 612 if(this.update.firstChild && this.update.firstChild.childNodes) { 613 this.entry_count = 614 this.update.firstChild.childNodes.length; 615 for (var i = 0; i < this.entry_count; i++) { 616 entry = this.get_entry(i); 617 entry.autocompleteIndex = i; 618 Event.observeHover(entry, this.onHover.bindAsEventListener(this)); 619 Event.observeClick(entry, this.onClick.bindAsEventListener(this)); 620 } 621 } else { 622 this.entry_count = 0; 623 } 624 625 this.stopIndicator(); 626 627 this.index = 0; 628 this.render(); 629 } 630 }, 631 632 onKeyPress: function(event) { 633 if(this.active) 634 switch(event.keyCode) { 635 case Event.KEY_TAB: 636 case Event.KEY_RETURN: 637 this.select_entry(); 638 Event.stop(event); 639 case Event.KEY_ESC: 640 this.hide(); 641 this.active = false; 642 return; 643 case Event.KEY_LEFT: 644 case Event.KEY_RIGHT: 645 return; 646 case Event.KEY_UP: 647 this.mark_previous(); 648 this.render(); 649 return; 650 case Event.KEY_DOWN: 651 this.mark_next(); 652 this.render(); 653 return; 654 } 655 else 656 if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) 657 return; 658 659 this.changed = true; 660 this.has_focus = true; 661 662 if(this.observer) clearTimeout(this.observer); 663 this.observer = 664 setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); 665 }, 666 667 onHover: function(event) { 668 element = Event.getParentNodeOrSelfByName(event, 'LI'); 669 if(this.index != element.autocompleteIndex) 670 { 671 this.index = element.autocompleteIndex; 672 this.render(); 673 } 674 }, 675 676 onClick: function(event) { 677 element = Event.getParentNodeOrSelfByName(event, 'LI'); 678 this.index = element.autocompleteIndex; 679 this.select_entry(); 680 }, 681 682 onBlur: function(event) { 683 element = Event.element(event); 684 if(element==this.update) return; 685 while(element.parentNode) 686 { element = element.parentNode; if(element==this.update) return; } 687 this.hide(); 688 this.active = false; 689 }, 690 691 render: function() { 692 if(this.entry_count > 0) { 693 for (var i = 0; i < this.entry_count; i++) 694 this.get_entry(i).className = 695 this.index==i ? 'selected' : ''; 696 697 if(this.has_focus) { 698 if(this.get_current_entry().scrollIntoView) 699 this.get_current_entry().scrollIntoView(false); 700 701 this.show(); 702 this.active = true; 703 } 704 } else this.hide(); 705 }, 706 707 mark_previous: function() { 708 if(this.index > 0) this.index-- 709 else this.index = this.entry_count-1; 710 }, 711 712 mark_next: function() { 713 if(this.index < this.entry_count-1) this.index++ 714 else this.index = 0; 715 }, 716 717 get_entry: function(index) { 718 return this.update.firstChild.childNodes[index]; 719 }, 720 721 get_current_entry: function() { 722 return this.get_entry(this.index); 723 }, 724 725 select_entry: function() { 726 this.hide(); 727 this.active = false; 728 value = Text.decodeHTML(Text.stripTags(this.get_current_entry().innerHTML)); 729 this.element.value = value; 730 this.element.focus(); 731 } 732 }); -
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 … … 151 153 def initialize(object_name, method_name, template_object, local_binding = nil) 152 154 @object_name, @method_name = object_name, method_name 153 155 @template_object, @local_binding = template_object, local_binding 156 @controller = @template_object.controller # url_for in :remote_autocomplete 154 157 if @object_name.sub!(/\[\]$/,"") 155 158 @auto_index = @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}").id_before_type_cast 156 159 end … … 166 169 options["type"] = field_type 167 170 options["value"] ||= value_before_type_cast unless field_type == "file" 168 171 add_default_name_and_id(options) 169 tag("input", options) 172 autocomplete_text = "" 173 if options["remote_autocomplete"] 174 if options["indicator"] 175 autocomplete_text << 176 tag("img", { :id => "#{options['id']}_autocomplete_indicator", :style => 'display:none;', :src => options["indicator"] }) 177 options.delete("indicator") 178 end 179 autocomplete_text << # content_tag needed to have <div></div> instead of <div/> 180 content_tag("div", "", { :id => "#{options['id']}_autocomplete", :class => "autocomplete", :style => "display:none;" }) 181 autocomplete_text << 182 autocomplete_for(options['id'], 183 { :url => options["remote_autocomplete"], :with => "'for=' + escape(value)", :indicator => "#{options['id']}_autocomplete_indicator" }) 184 options["autocomplete"] = "off" 185 options.delete("remote_autocomplete") 186 end 187 tag_text = tag("input", options) 188 tag_text << autocomplete_text 189 return tag_text 170 190 end 171 191 172 192 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 /*--------------------------------------------------------------------------*/ 457 458 Event = { 459 KEY_BACKSPACE: 8, 460 KEY_TAB: 9, 461 KEY_RETURN: 13, 462 KEY_ESC: 27, 463 KEY_LEFT: 37, 464 KEY_UP: 38, 465 KEY_RIGHT: 39, 466 KEY_DOWN: 40, 467 KEY_DELETE: 46, 468 469 element: function(event) { 470 return event.srcElement || event.currentTarget; 471 }, 472 473 stop: function(event) { 474 if(event.preventDefault) 475 { event.preventDefault(); event.stopPropagation(); } 476 else 477 event.returnValue = false; 478 }, 479 480 getParentNodeOrSelfByName: function(event, nodeName) { 481 element = Event.element(event); 482 while(element.nodeName != nodeName && element.parentNode) 483 element = element.parentNode; 484 return element; 485 }, 486 487 observeKeypress: function(element, observer) { 488 if(navigator.appVersion.indexOf('AppleWebKit')>0) 489 { $(element).addEventListener("keydown",observer,false); return; } 490 if($(element).addEventListener) $(element).addEventListener("keypress",observer,false) 491 else if($(element).attachEvent) $(element).attachEvent("onkeydown",observer); 492 }, 493 494 observeBlur: function(element, observer) { 495 if($(element).addEventListener) $(element).addEventListener("blur",observer,false) 496 else if($(element).attachEvent) $(element).attachEvent("onblur",observer); 497 }, 498 499 observeClick: function(element, observer) { 500 if($(element).addEventListener) $(element).addEventListener("click",observer,false) 501 else if($(element).attachEvent) $(element).attachEvent("onclick",observer); 502 }, 503 504 observeHover: function(element, observer) { 505 if($(element).addEventListener) $(element).addEventListener("mouseover",observer,false) 506 else if($(element).attachEvent) $(element).attachEvent("onmouseover",observer); 507 } 508 509 } 510 511 Text = { 512 stripTags: function(htmlstr) { 513 return htmlstr.replace(/<\/?[^>]+>/gi,""); 514 }, 515 decodeHTML: function(htmlstr) { 516 return htmlstr.replace("<","<").replace(">",">").replace(""",'"').replace("&","&"); 517 } 518 } 519 520 Element = { 521 show: function(element) { 522 $(element).style.display = ''; 523 }, 524 hide: function(element) { 525 $(element).style.display = 'none'; 526 }, 527 samePositionAs: function(element, aselement) { 528 if(navigator.appVersion.indexOf('MSIE')>0) { 529 $(element).style.top = $(aselement).style.top; 530 $(element).style.left = $(aselement).style.left; 531 $(element).style.width = $(aselement).offsetWidth; 532 $(element).style.height = $(aselement).offsetHeight; 533 } 534 } 535 } 536 537 Ajax.Autocomplete = Class.create(); 538 Ajax.Autocomplete.prototype = (new Ajax.Base()).extend({ 539 initialize: function(element, update, url, options) { 540 this.element = $(element); 541 this.update = $(update); 542 this.has_focus = false; 543 this.changed = false; 544 this.active = false; 545 this.index = 0; 546 this.entry_count = 0; 547 this.url = url; 548 549 this.setOptions(options); 550 this.options.asynchronous = true; 551 this.options.onComplete = this.onComplete.bind(this) 552 this.options.frequency = this.options.frequency || 0.4; 553 this.options.min_chars = this.options.min_chars || 1; 554 this.options.method = 'post'; 555 556 if(this.options.indicator) 557 this.indicator = $(this.options.indicator); 558 559 this.observer = null; 560 561 Event.observeKeypress (this.element, this.onKeyPress.bindAsEventListener(this)); 562 Event.observeClick (document, this.onBlur.bindAsEventListener(this)); 563 }, 564 565 show: function() { 566 Element.show(this.update); 567 if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0)) { 568 new Insertion.Before(this.update, 569 '<iframe id="' + this.update.id + '_iefix" style="display:none;" src="javascript:false;" frameborder="0" scrolling="no"></iframe>'); 570 this.iefix = $(this.update.id+'_iefix'); 571 this.iefix.style.position = 'absolute'; 572 this.iefix.style.zIndex = 1; 573 this.update.style.zIndex = 2; 574 } 575 if(this.iefix) { 576 Element.samePositionAs(this.iefix, this.update); 577 Element.show(this.iefix); 578 } 579 }, 580 581 hide: function() { 582 if(this.iefix) Element.hide(this.iefix); 583 Element.hide(this.update); 584 }, 585 586 startIndicator: function() { 587 if(this.indicator) Element.show(this.indicator); 588 }, 589 590 stopIndicator: function() { 591 if(this.indicator) Element.hide(this.indicator); 592 }, 593 594 onObserverEvent: function() { 595 this.changed = false; 596 if(this.element.value.length>=this.options.min_chars) { 597 this.startIndicator(); 598 this.options.parameters = this.options.callback ? 599 this.options.callback(this.element, Form.Element.getValue(this.element)) : 600 Form.Element.getValue(this.element); 601 new Ajax.Request(this.url, this.options); 602 } else { 603 this.active = false; 604 this.hide(); 605 } 606 }, 607 608 onComplete: function(request) { 609 if(!this.changed) { 610 this.update.innerHTML = request.responseText; 611 612 if(this.update.firstChild && this.update.firstChild.childNodes) { 613 this.entry_count = 614 this.update.firstChild.childNodes.length; 615 for (var i = 0; i < this.entry_count; i++) { 616 entry = this.get_entry(i); 617 entry.autocompleteIndex = i; 618 Event.observeHover(entry, this.onHover.bindAsEventListener(this)); 619 Event.observeClick(entry, this.onClick.bindAsEventListener(this)); 620 } 621 } else { 622 this.entry_count = 0; 623 } 624 625 this.stopIndicator(); 626 627 this.index = 0; 628 this.render(); 629 } 630 }, 631 632 onKeyPress: function(event) { 633 if(this.active) 634 switch(event.keyCode) { 635 case Event.KEY_TAB: 636 case Event.KEY_RETURN: 637 this.select_entry(); 638 Event.stop(event); 639 case Event.KEY_ESC: 640 this.hide(); 641 this.active = false; 642 return; 643 case Event.KEY_LEFT: 644 case Event.KEY_RIGHT: 645 return; 646 case Event.KEY_UP: 647 this.mark_previous(); 648 this.render(); 649 return; 650 case Event.KEY_DOWN: 651 this.mark_next(); 652 this.render(); 653 return; 654 } 655 else 656 if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) 657 return; 658 659 this.changed = true; 660 this.has_focus = true; 661 662 if(this.observer) clearTimeout(this.observer); 663 this.observer = 664 setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); 665 }, 666 667 onHover: function(event) { 668 element = Event.getParentNodeOrSelfByName(event, 'LI'); 669 if(this.index != element.autocompleteIndex) 670 { 671 this.index = element.autocompleteIndex; 672 this.render(); 673 } 674 }, 675 676 onClick: function(event) { 677 element = Event.getParentNodeOrSelfByName(event, 'LI'); 678 this.index = element.autocompleteIndex; 679 this.select_entry(); 680 }, 681 682 onBlur: function(event) { 683 element = Event.element(event); 684 if(element==this.update) return; 685 while(element.parentNode) 686 { element = element.parentNode; if(element==this.update) return; } 687 this.hide(); 688 this.active = false; 689 }, 690 691 render: function() { 692 if(this.entry_count > 0) { 693 for (var i = 0; i < this.entry_count; i++) 694 this.get_entry(i).className = 695 this.index==i ? 'selected' : ''; 696 697 if(this.has_focus) { 698 if(this.get_current_entry().scrollIntoView) 699 this.get_current_entry().scrollIntoView(false); 700 701 this.show(); 702 this.active = true; 703 } 704 } else this.hide(); 705 }, 706 707 mark_previous: function() { 708 if(this.index > 0) this.index-- 709 else this.index = this.entry_count-1; 710 }, 711 712 mark_next: function() { 713 if(this.index < this.entry_count-1) this.index++ 714 else this.index = 0; 715 }, 716 717 get_entry: function(index) { 718 return this.update.firstChild.childNodes[index]; 719 }, 720 721 get_current_entry: function() { 722 return this.get_entry(this.index); 723 }, 724 725 select_entry: function() { 726 this.hide(); 727 this.active = false; 728 value = Text.decodeHTML(Text.stripTags(this.get_current_entry().innerHTML)); 729 this.element.value = value; 730 this.element.focus(); 731 } 732 }); -
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; -
actionpack/lib/action_view/helpers/javascripts/prototype.js
old new 452 452 element.style.backgroundColor = "#ffff" + current.toColorPart(); 453 453 } 454 454 } 455 456 /*--------------------------------------------------------------------------*/ 457 458 Event = { 459 KEY_BACKSPACE: 8, 460 KEY_TAB: 9, 461 KEY_RETURN: 13, 462 KEY_ESC: 27, 463 KEY_LEFT: 37, 464 KEY_UP: 38, 465 KEY_RIGHT: 39, 466 KEY_DOWN: 40, 467 KEY_DELETE: 46, 468 469 element: function(event) { 470 return event.srcElement || event.currentTarget; 471 }, 472 473 stop: function(event) { 474 if(event.preventDefault) 475 { event.preventDefault(); event.stopPropagation(); } 476 else 477 event.returnValue = false; 478 }, 479 480 getParentNodeOrSelfByName: function(event, nodeName) { 481 element = Event.element(event); 482 while(element.nodeName != nodeName && element.parentNode) 483 element = element.parentNode; 484 return element; 485 }, 486 487 observeKeypress: function(element, observer) { 488 if(navigator.appVersion.indexOf('AppleWebKit')>0) 489 { $(element).addEventListener("keydown",observer,false); return; } 490 if($(element).addEventListener) $(element).addEventListener("keypress",observer,false) 491 else if($(element).attachEvent) $(element).attachEvent("onkeydown",observer); 492 }, 493 494 observeBlur: function(element, observer) { 495 if($(element).addEventListener) $(element).addEventListener("blur",observer,false) 496 else if($(element).attachEvent) $(element).attachEvent("onblur",observer); 497 }, 498 499 observeClick: function(element, observer) { 500 if($(element).addEventListener) $(element).addEventListener("click",observer,false) 501 else if($(element).attachEvent) $(element).attachEvent("onclick",observer); 502 }, 503 504 observeHover: function(element, observer) { 505 if($(element).addEventListener) $(element).addEventListener("mouseover",observer,false) 506 else if($(element).attachEvent) $(element).attachEvent("onmouseover",observer); 507 } 508 509 } 510 511 Text = { 512 stripTags: function(htmlstr) { 513 return htmlstr.replace(/<\/?[^>]+>/gi,""); 514 }, 515 decodeHTML: function(htmlstr) { 516 return htmlstr.replace("<","<").replace(">",">").replace(""",'"').replace("&","&"); 517 } 518 } 519 520 Element = { 521 show: function(element) { 522 $(element).style.display = ''; 523 }, 524 hide: function(element) { 525 $(element).style.display = 'none'; 526 }, 527 samePositionAs: function(element, aselement) { 528 if(navigator.appVersion.indexOf('MSIE')>0) { 529 $(element).style.top = $(aselement).style.top; 530 $(element).style.left = $(aselement).style.left; 531 $(element).style.width = $(aselement).offsetWidth; 532 $(element).style.height = $(aselement).offsetHeight; 533 } 534 } 535 } 536 537 Ajax.Autocomplete = Class.create(); 538 Ajax.Autocomplete.prototype = (new Ajax.Base()).extend({ 539 initialize: function(element, update, url, options) { 540 this.element = $(element); 541 this.update = $(update); 542 this.has_focus = false; 543 this.changed = false; 544 this.active = false; 545 this.index = 0; 546 this.entry_count = 0; 547 this.url = url; 548 549 this.setOptions(options); 550 this.options.asynchronous = true; 551 this.options.onComplete = this.onComplete.bind(this) 552 this.options.frequency = this.options.frequency || 0.4; 553 this.options.min_chars = this.options.min_chars || 1; 554 this.options.method = 'post'; 555 556 if(this.options.indicator) 557 this.indicator = $(this.options.indicator); 558 559 this.observer = null; 560 561 Event.observeKeypress (this.element, this.onKeyPress.bindAsEventListener(this)); 562 Event.observeClick (document, this.onBlur.bindAsEventListener(this)); 563 }, 564 565 show: function() { 566 Element.show(this.update); 567 if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0)) { 568 new Insertion.Before(this.update, 569 '<iframe id="' + this.update.id + '_iefix" style="display:none;" src="javascript:false;" frameborder="0" scrolling="no"></iframe>'); 570 this.iefix = $(this.update.id+'_iefix'); 571 this.iefix.style.position = 'absolute'; 572 this.iefix.style.zIndex = 1; 573 this.update.style.zIndex = 2; 574 } 575 if(this.iefix) { 576 Element.samePositionAs(this.iefix, this.update); 577 Element.show(this.iefix); 578 } 579 }, 580 581 hide: function() { 582 if(this.iefix) Element.hide(this.iefix); 583 Element.hide(this.update); 584 }, 585 586 startIndicator: function() { 587 if(this.indicator) Element.show(this.indicator); 588 }, 589 590 stopIndicator: function() { 591 if(this.indicator) Element.hide(this.indicator); 592 }, 593 594 onObserverEvent: function() { 595 this.changed = false; 596 if(this.element.value.length>=this.options.min_chars) { 597 this.startIndicator(); 598 this.options.parameters = this.options.callback ? 599 this.options.callback(this.element, Form.Element.getValue(this.element)) : 600 Form.Element.getValue(this.element); 601 new Ajax.Request(this.url, this.options); 602 } else { 603 this.active = false; 604 this.hide(); 605 } 606 }, 607 608 onComplete: function(request) { 609 if(!this.changed) { 610 this.update.innerHTML = request.responseText; 611 612 if(this.update.firstChild && this.update.firstChild.childNodes) { 613 this.entry_count = 614 this.update.firstChild.childNodes.length; 615 for (var i = 0; i < this.entry_count; i++) { 616 entry = this.get_entry(i); 617 entry.autocompleteIndex = i; 618 Event.observeHover(entry, this.onHover.bindAsEventListener(this)); 619 Event.observeClick(entry, this.onClick.bindAsEventListener(this)); 620 } 621 } else { 622 this.entry_count = 0; 623 } 624 625 this.stopIndicator(); 626 627 this.index = 0; 628 this.render(); 629 } 630 }, 631 632 onKeyPress: function(event) { 633 if(this.active) 634 switch(event.keyCode) { 635 case Event.KEY_TAB: 636 case Event.KEY_RETURN: 637 this.select_entry(); 638 Event.stop(event); 639 case Event.KEY_ESC: 640 this.hide(); 641 this.active = false; 642 return; 643 case Event.KEY_LEFT: 644 case Event.KEY_RIGHT: 645 return; 646 case Event.KEY_UP: 647 this.mark_previous(); 648 this.render(); 649 return; 650 case Event.KEY_DOWN: 651 this.mark_next(); 652 this.render(); 653 return; 654 } 655 else 656 if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) 657 return; 658 659 this.changed = true; 660 this.has_focus = true; 661 662 if(this.observer) clearTimeout(this.observer); 663 this.observer = 664 setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); 665 }, 666 667 onHover: function(event) { 668 element = Event.getParentNodeOrSelfByName(event, 'LI'); 669 if(this.index != element.autocompleteIndex) 670 { 671 this.index = element.autocompleteIndex; 672 this.render(); 673 } 674 }, 675 676 onClick: function(event) { 677 element = Event.getParentNodeOrSelfByName(event, 'LI'); 678 this.index = element.autocompleteIndex; 679 this.select_entry(); 680 }, 681 682 onBlur: function(event) { 683 element = Event.element(event); 684 if(element==this.update) return; 685 while(element.parentNode) 686 { element = element.parentNode; if(element==this.update) return; } 687 this.hide(); 688 this.active = false; 689 }, 690 691 render: function() { 692 if(this.entry_count > 0) { 693 for (var i = 0; i < this.entry_count; i++) 694 this.get_entry(i).className = 695 this.index==i ? 'selected' : ''; 696 697 if(this.has_focus) { 698 if(this.get_current_entry().scrollIntoView) 699 this.get_current_entry().scrollIntoView(false); 700 701 this.show(); 702 this.active = true; 703 } 704 } else this.hide(); 705 }, 706 707 mark_previous: function() { 708 if(this.index > 0) this.index-- 709 else this.index = this.entry_count-1; 710 }, 711 712 mark_next: function() { 713 if(this.index < this.entry_count-1) this.index++ 714 else this.index = 0; 715 }, 716 717 get_entry: function(index) { 718 return this.update.firstChild.childNodes[index]; 719 }, 720 721 get_current_entry: function() { 722 return this.get_entry(this.index); 723 }, 724 725 select_entry: function() { 726 this.hide(); 727 this.active = false; 728 value = Text.decodeHTML(Text.stripTags(this.get_current_entry().innerHTML)); 729 this.element.value = value; 730 this.element.focus(); 731 } 732 }); -
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