{"id":11,"date":"2007-01-31T23:08:08","date_gmt":"2007-01-31T22:08:08","guid":{"rendered":"http:\/\/schuerig.de\/michael\/blog\/?p=11"},"modified":"2021-11-24T09:20:10","modified_gmt":"2021-11-24T08:20:10","slug":"auto-completion-for-tag-lists","status":"publish","type":"post","link":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/2007\/01\/31\/auto-completion-for-tag-lists\/","title":{"rendered":"Auto-completion for tag lists"},"content":{"rendered":"<p><em>See the end of this post for an important update.<\/em><\/p>\n<p>Standard Rails auto-completion is geared toward completing a single value entered in a text field. For most cases this is exactly what&#8217;s needed, but there is one prominent case that is different: auto-completion of tags. If something can have a tag at all, it&#8217;s part and parcel of the deal that it can have multiple tags.<\/p>\n<p>As far as I can tell, some configuration tweak is not enough to make standard auto-completion do our bidding. But not all is lost. It&#8217;s just a small matter of programming to get what we want.<\/p>\n<p>I&#8217;m assuming that you&#8217;re using <a href=\"http:\/\/www.agilewebdevelopment.com\/plugins\/acts_as_taggable\"><tt>Acts As Taggable<\/tt><\/a> or <a href=\"http:\/\/www.agilewebdevelopment.com\/plugins\/acts_as_taggable_on_steroids\"><tt>Acts As Taggable On Steroids<\/tt><\/a> for the model-level tagging, or otherwise that you know what you&#8217;re doing.<\/p>\n<p>First, we need client-side support for picking out only the tag we&#8217;re currently editing.<\/p>\n<p><code><\/p>\n<pre>\r\nAjax.TagAutocompleter = Class.create();\r\nObject.extend(Object.extend(Ajax.TagAutocompleter.prototype, Ajax.Autocompleter.prototype), {\r\n  getToken: function() {\r\n    var range = this.rangeForCaret();\r\n    var value = this.element.value.substring(range.start, range.end);\r\n    return value;\r\n  },\r\n  updateElement: function(selectedElement) {\r\n    var oldValue = this.element.value;\r\n    var range = this.rangeForCaret();\r\n    var prefix = oldValue.substring(0, range.start);\r\n    var suffix = oldValue.substring(range.end);\r\n    var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');\r\n    this.element.value = prefix + value + suffix;\r\n    this.element.focus();\r\n  },\r\n  rangeForCaret: function() {\r\n    var value = this.element.value;\r\n    var start = 0, end = value.length;\r\n    var pos;\r\n    if (typeof this.element.selectionStart != 'undefined') {\r\n      pos = this.element.selectionStart;\r\n    } else if (document.selection) {\r\n      var sel = document.selection.createRange();\r\n      var selLength = sel.text.length;\r\n      sel.moveStart('character', -(value.length));\r\n      pos = sel.text.length - selLength;\r\n    }\r\n    if (pos != undefined) {\r\n      end = pos;\r\n      while (value.charAt(end) != ',' &amp;&amp; end &lt; value.length) {\r\n        end++;\r\n      }\r\n      start = end - 1;\r\n      while (value.charAt(start) != ',' &amp;&amp; start &gt; 0) {\r\n        start--;\r\n      }\r\n      while (start &lt; end &amp;&amp; (value.charAt(start) == ' ' || value.charAt(start) == ',')) {\r\n        start++;\r\n      }\r\n      while (start &lt; end &amp;&amp; value.charAt(end) == ' ') {\r\n        end--;\r\n      }\r\n    }\r\n    return {start: start, end: end};\r\n  }\r\n});\r\n<\/pre>\n<p><\/code><\/p>\n<p>Yes, the code of <tt>rangeForCaret<\/tt> is a bit convoluted, let&#8217;s say traditional JavaScript-style. The general approach for getting the position of the text cursor (&#8220;caret&#8221;) is explained <a href=\"http:\/\/www.theblueform.com\/Home\/TheMakingOf.aspx\">here<\/a>. The purpose of all the stuff is to support auto-completion in the middle of the text field, not just at its end.<\/p>\n<p>On the server-side, in the relevant controller, there&#8217;s still some work to do as the standard <tt>auto_complete_for<\/tt> can&#8217;t handle tags. Put the following method in your controller base class, <tt>app\/controllers\/application.rb<\/tt>.<\/p>\n<p><code><\/p>\n<pre>\r\ndef self.tag_auto_complete_for(object)\r\n  define_method(\"auto_complete_for_#{object}_tag_list\") do\r\n    @items = Tag.find(:all,\r\n      :conditions => [ 'LOWER(name) LIKE ?', '%' + params[object][:tag_list].downcase + '%' ], \r\n      :\\order => 'name ASC',\r\n      :limit => 10)\r\n    render :inline => \"&lt;%= auto_complete_result @items, :name %&gt;\"\r\n  end\r\nend\r\n<\/pre>\n<p><\/code><\/p>\n<p>Then, in an individual controller, create the tag auto-completion method like this<\/p>\n<p><code><\/p>\n<pre>\r\nclass PeopleController &lt; ApplicationController\r\n  tag_auto_complete_for :person\r\nend\r\n<\/pre>\n<p><\/code><\/p>\n<p>Almost done!<\/p>\n<p>What&#8217;s still missing is a way to activate auto-completion for the tag input field. It would surely be possible to copy &amp; change <tt>text_field_with_auto_complete<\/tt> or to monkey patch it appropriately. I prefer another way, in fact, I&#8217;m not using <tt>text_field_with_auto_complete<\/tt> at all, not even where it would work. Instead, I prefer more unobtrusive way.<\/p>\n<p>The way starts with the class <tt>autocomplete<\/tt> to text fields that are amenable to auto-completion &#8212; including, of course, the tag list field.<\/p>\n<p><code><\/p>\n<pre>\r\n  &lt;%= f.text_field :tag_list, :class => 'autocomplete' %&gt;\r\n<\/pre>\n<p><\/code><\/p>\n<p>As this in itself is completely inert, we need to nudge it a bit in <tt>application.js<\/tt>. Here&#8217;s a function that looks for all text fields with class <tt>autocomplete<\/tt> and actually makes them do what they say.<\/p>\n<p><code><\/p>\n<pre>\r\nfunction installAutocompletion() {\r\n  $$('input.autocomplete[type=text]').each(function(element) {\r\n    var fieldId = element.id;\r\n    var completions = document.createElement('div');\r\n    completions.id = fieldId + '_auto_complete';\r\n    completions.className = 'auto_complete';\r\n    completions.style.display = 'none';\r\n    element.parentNode.insertBefore(completions, element.nextSibling);\r\n\r\n    var url = 'auto_complete_for_' + fieldId;\r\n    if (\/_tag_list$\/.test(fieldId)) {\r\n      new Ajax.TagAutocompleter(element, completions, url);\r\n    } else {\r\n      new Ajax.Autocompleter(element, completions, url);\r\n    }\r\n  });\r\n}\r\n<\/pre>\n<p><\/code><\/p>\n<p>Be sure to call this method some time after the page is loaded. If you don&#8217;t have a function from where to call it, add this to <tt>application.js<\/tt><\/p>\n<p><code><\/p>\n<pre>\r\n  Event.observe(window, 'load', installAutocompletion);\r\n<\/pre>\n<p><\/code><\/p>\n<h3>Update, 2007-02-05<\/h3>\n<p>I may not have seen the wood for the trees when looking at the original code of the Rails\/Script.aculo.us autocompleter. Of course it is fully capable of auto-completing more than one token in the same text field &#8212; with a very little configuration tweak, no less.<\/p>\n<p>What it can&#8217;t do is auto-completion in the middle of the line; it only works at the end. That&#8217;s an acceptable restriction, to my mind. So, forget about <code>Ajax.TagAutocompleter<\/code> from above and instead install your autocompleters like this<\/p>\n<p><code><\/p>\n<pre>\r\nfunction installAutocompletion() {\r\n  $$('input.autocomplete[type=text]').each(function(element) {\r\n    var fieldId = element.id;\r\n    var completions = document.createElement('div');\r\n    completions.id = fieldId + '_auto_complete';\r\n    completions.className = 'auto_complete';\r\n    completions.style.display = 'none';\r\n    element.parentNode.insertBefore(completions, element.nextSibling);\r\n\r\n    var url = 'auto_complete_for_' + fieldId;\r\n    var options = {};\r\n    if (\/_tag_list$\/.test(fieldId)) {\r\n      options.tokens = ',';\r\n    }\r\n    new Ajax.Autocompleter(element, completions, url, options);\r\n  });\r\n}\r\n<\/pre>\n<p><\/code><\/p>\n","protected":false},"excerpt":{"rendered":"<p>See the end of this post for an important update. Standard Rails auto-completion is geared toward completing a single value entered in a text field. For most cases this is exactly what&#8217;s needed, but there is one prominent case that &hellip; <a href=\"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/2007\/01\/31\/auto-completion-for-tag-lists\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[8,5],"tags":[],"class_list":["post-11","post","type-post","status-publish","format-standard","hentry","category-javascript","category-rails"],"_links":{"self":[{"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/posts\/11","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/comments?post=11"}],"version-history":[{"count":1,"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/posts\/11\/revisions"}],"predecessor-version":[{"id":150,"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/posts\/11\/revisions\/150"}],"wp:attachment":[{"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/media?parent=11"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/categories?post=11"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/tags?post=11"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}