Typo Administration: VI...final-ish

Posted by Steve Longdo Mon, 27 Mar 2006 05:03:00 GMT

I did make some additonal changes in an attempt to get the functionality included, but it doesn't look like auto-complete tagging will make it into Typo 4.0. Which is too bad. Team Typo didn't want the capability to see all of the tags at once. So, I went back to overriding the Autocompleter.Local class directly in the _form.rhtml partial. Since the code was unlikely to be used anywhere outside of there and it needed less modficiation to achieve the desired effect of completing only what was typed.

I did expand the capabilties of the autocomplete code to make it a bit more usable. I included the number of times the tag has been used with articles in parenthesis after the tag name:
Codewise the plumbing was already in place, but was forgotten during the implementation. The tag name includes the article counter as a tilda(~) delimited string:
opts = new Array(<%= @tags.sort_by{|t| t.display_name}.collect!{|t1| '"' + t1.display_name + '~' + t1.article_counter.to_s + '",'} -%>'');
...
ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + elem.slice(entry.length, elem.indexOf('~')) + "<span class=\"informal\">  (" + elem.substr(elem.indexOf('~') + 1 , elem.length) +  ")</span></li>");
Under the General Settings on the admin page I made it possible to configure the autocomplete:
Perhaps the patch will make it into Typo 4.1. Take a look at the full diff of the patch in pretty print.

Typo Administration: article tagging V...

Posted by Steve Longdo Sun, 19 Mar 2006 20:43:00 GMT

Originally we pursued using the Autocompleter.Local from thescript.aculo.us library. This had some promising results. However I wanted to be able to do a listing of all of the tags in a scrollable field not just the matches to what I typed. For example: I typed in 'r' and matched 'ruby' but I can also see 'sql' which might also be a good tag to add to the posting I am working on.

This led me to extend Autocompleter.Base instead and add the desired functionality myself. Adding scrolling proved a bit tricky, mainly because the offsetHeight property of the element tags always returns zero. I think this is because it is inside of an absolutely positioned DIV, but I am not a CSS guru (believe it or not:-P). I created an AdminAutotag class that is a replica of the Autocompleter.Local class. The selector function needs to be radically altered to support showing all tags and not just matches. Also in this setup it wouldn't make sense to support partial matches so we will remove that. We can still handle case sensitivity though. Since we want this to eventually to end up in Typo, instead of putting all of the JavaScript directly on the _form.rhtml partial, lets move it instead to the typo.js file in /public.
public/javascript/typo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
AdminAutotag = Class.create();
AdminAutotag.prototype = Object.extend(new Autocompleter.Base(), {
	initialize: function(element, update, array, options) {
	  this.baseInitialize(element, update, options);
	  this.options.array = array;
	},
  getUpdatedChoices: function() {
    this.updateChoices(this.options.selector(this));
  },
  setOptions: function(options) {
    this.options = Object.extend({
      ignoreCase: true,
	/* start selector */

      selector: function(instance) {
	  var ret       = []; // Beginning matches
	  var entry     = instance.getToken();
	  var firstMatch= -1;
	  for (var i = 0; i < instance.options.array.length; i++) { 
		var wasMatch = false;
	    var elem = instance.options.array[i];
	    var foundPos = instance.options.ignoreCase ? 
	      elem.toLowerCase().indexOf(entry.toLowerCase()) : elem.indexOf(entry);

	    while (foundPos != -1) {
	      if (foundPos == 0 && elem.length != entry.length) { 
			wasMatch = true;
			if (firstMatch < 0){
			  firstMatch=i;
			  instance.index=i;
	          ret.push("<li class=\"selected\"><strong>" + elem.substr(0, entry.length) + "</strong>" + 
	              elem.substr(entry.length) + "</li>");
			} else {
		        ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + 
		          elem.substr(entry.length) + "</li>");
			}
	        break;
	      }      

	      foundPos = instance.options.ignoreCase ? 
	        elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
	        elem.indexOf(entry, foundPos + 1);
	    }
		if(wasMatch==false){
	    	ret.push("<li>" + elem + "</li>");
		}
	  }
	  instance.element.autocompleteIndex=firstMatch;
	  instance.index=firstMatch;
	  return "<ul>" + ret.join('') + "</ul>";
	}
	//end

    }, options || {});
  }
});
Besides removing the partial matching code, the primary difference here is that we are pushing out elements whether or not they match (#43-#45). Notice also lines 47 and 48 that set our first match. This how we will keep track of where to scroll to. In fact how are we going to handle that? We will have to control the rendering of the component so we will need to override that method. Our render function will accept a parameter, unlike the base version. This is to handle the auto-scrolling via mouse pointer capability of the DIV I mentioned before. This will allow us to selectively allow scrolling, i.e. not onMouseover events. Besides removing the partial matching code, the primary difference here is that we are pushing out elements whether or not they match (#43-#45). Notice also lines 47 and 48 that set our first match. This how we will keep track of where to scroll to. In fact how are we going to handle that? We will have to control the rendering of the component so we will need to override that method. Our render function will accept a parameter, unlike the base version. This is to handle the auto-scrolling via mouse pointer capability of the DIV I mentioned before. This will allow us to selectively allow scrolling, i.e. not onMouseover events.
Inside AdminAutotag.prototype
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  render: function(scrollActive) {
	if(this.index==0 && this.options.picked!=0) this.index = this.element.autocompleteIndex;
    if(this.entryCount > 0) {
      for (var i = 0; i < this.entryCount; i++)

        i==this.index ? 
          Element.addClassName(this.getEntry(i),"selected") : 
          Element.removeClassName(this.getEntry(i),"selected");

      if(this.hasFocus) { 
        this.show();
        this.active = true;
        //scrolling for key events only

		if(scrollActive==null || scrollActive==true) {
			var scrollAmt = this.index > 0 ? (this.index * 24) - 2 : 0;
			this.update.scrollTop=scrollAmt;
		}
	  } else {
	      this.active = false;
	      this.hide();
	  }
	}
  },
Due to the aforementioned difficulty in getting the offsetHeight of a single LI in our list there is a bit of a kludge at line #14. The index of the currently selected item times twenty four minus two seemed to work well in the browsers I tested with fairly large numbers of tags. Post a comment if you know the right way to do it. Also at line #2 there is a reference to a field we haven't seen yet (this.options.picked) we need to use this field to keep up with keyboard events...which we will need to override as well. Speaking of events we need to make sure and take care of mouse events by overriding onHover:
Inside AdminAutotag.prototype
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  render: function(scrollActive) {
	if(this.index==0 && this.options.picked!=0) this.index = this.element.autocompleteIndex;
    if(this.entryCount > 0) {
      for (var i = 0; i < this.entryCount; i++)

        i==this.index ? 
          Element.addClassName(this.getEntry(i),"selected") : 
          Element.removeClassName(this.getEntry(i),"selected");

      if(this.hasFocus) { 
        this.show();
        this.active = true;
        //scrolling for key events only


		if(scrollActive==null || scrollActive==true) {
			var scrollAmt = this.index > 0 ? (this.index * 24) - 2 : 0;
			this.update.scrollTop=scrollAmt;
		}
	  } else {
	      this.active = false;
	      this.hide();
	  }
	}
  },
These are almost the same as their counterparts in Base, but the mark functions now update our this.options.picked, and our onHover passes false to the render method so as not to have you chasing the scrolling list of tags when you use the mouse. Back to our _form.rhtml for the keywords field, it can now be simplified to just this:
<p>
  <label for="article_keywords">Keywords:</label><br/>
  <%= text_field 'article', 'keywords'  %>
<div id="auto_complete"></div>
<script type="text/javascript" language="javascript" charset="utf-8">
  opts = new Array(<%= @tags.sort_by{|t| t.display_name}.collect!{|t1| '"' + t1.display_name + '",'} -%>'');
  new AdminAutotag('article_keywords','auto_complete', opts , { tokens: new Array(' ',',','\n')});
</script>
</p>
Since we use this functionality in admin, I put the CSS for the autocomplete there:
 /* public/stylesheets/administration.css */

#auto_complete {
  width: 355px;
  height: 150px;
  background: #ffffff;
  overflow: auto;
}

#auto_complete ul {
  border: 1px solid #888888;
  margin:0;
  padding:0;
  width: 98%;
  list-style-type:none;
}

#auto_complete ul li {
  margin:0;
  padding:0;
}

#auto_complete ul li.selected {
  background-color: #d7dfe4;
}
#auto_complete ul strong.highlight {
  color: #880000;
  margin:0;
  padding:0;
}

That is all there is too it. Seems simple now huh? The above code has been packed up as a patch against Typo and is detailed in ticket #727.

Typo Admin: tagging IV and Magnolia update

Posted by Steve Longdo Sun, 19 Mar 2006 07:14:00 GMT

The Autocompleter.Local from script.aculo.us has been a great foundation, forcing me to get current with my JavaScript-fu in order to make it do what I want. I have been spending a lot of time customizing its functionality to make for a better user experience. I accidentally created a DIV that will scroll following the mouse pointer! I need to make it so it only does this when you are near the top or bottom of the DIV and scrolling actually makes sense. After I get that cleaned up I will write the final post about auto-tagging for Typo and will submit the code as a patch. Sorry for the long delay.

I have gotten a lot of requests for the rating code I wrote about some time ago. I am going to pick it back up and polish it up so that it is shareable. I will probably write it up like a how-to article and create an Articles section for the site.

Additionally I would like to promote the Magnolia sidebar on this page. Magnolia is quite a bit like del.icio.us, except more community oriented. Also they save images of what the page looked like when you bookmarked it. An extremely useful feature, not to mention that they allow you to have private bookmarks if you are feeling anti-social, and they provide a way to import your del.icio.us bookmarks into their service :-)

The Magnolia sidebar on my page is currently using the javascript from Damien Tanner, however I have just completed an actual Typo sidebar for based on it. It has been submitted as patch #723. Hopefully it will make it into the upcoming 4.0 release.

Older posts: 1 2