radiant-tags-extension 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README CHANGED
@@ -3,7 +3,6 @@
3
3
  Created by: Keith Bingman - keithbingman.com
4
4
  Revived by: Benny Degezelle - gorilla-webdesign.be
5
5
  New features by: Jim Gay - saturnflyer.com
6
- Version: 1.5
7
6
 
8
7
  This extension enhances the page model with tagging capabilities, tagging as in "2.0" and tagclouds.
9
8
 
@@ -15,7 +14,16 @@ You can change the load order of extensions in config/environment.rb (see http:/
15
14
 
16
15
  == Installation
17
16
 
18
- 1. Copy the extension to your vendor/extensions directory as you would any other extension.
17
+ === Using RubyGems
18
+
19
+ `gem install radiant-tags-extension`
20
+
21
+ add the following line to your environment.rb
22
+ config.gem 'radiant-tags-extension'
23
+
24
+ === Classic style
25
+
26
+ 1. Copy the extension to your vendor/extensions directory as you would any other extension or use `script/extension install tags`
19
27
  2. Run 'rake radiant:extensions:tags:install'
20
28
  3. Make a page to sit in /search/by-tag, and give it the "Tag Search" pagetype.
21
29
  If you want to change this location, it's in Radiant::Config['tags.results_page_url'].
@@ -35,4 +43,4 @@ Here's a sample results page to get you started;
35
43
  <li><r:link/> - <r:author/> - <r:date/></li>
36
44
  </r:search:results:each>
37
45
  </ul>
38
- </r:search:results>
46
+ </r:search:results>
@@ -36,18 +36,38 @@ class MetaTag < ActiveRecord::Base
36
36
  end
37
37
 
38
38
  def cloud(args = {})
39
+ args = {:by => 'popularity', :order => 'desc', :limit => 5}.merge(args)
40
+
39
41
  find(:all, :select => 'meta_tags.*, count(*) as popularity',
40
- :limit => args[:limit] || 5,
42
+ :limit => args[:limit],
41
43
  :joins => "JOIN taggings ON taggings.meta_tag_id = meta_tags.id",
42
44
  :conditions => args[:conditions],
43
45
  :group => "meta_tags.id, meta_tags.name",
44
- :order => "popularity DESC" )
46
+ :order => order_string(args) )
47
+ end
48
+
49
+ def order_string(attr)
50
+ by = (attr[:by]).strip
51
+ order = (attr[:order]).strip
52
+ order_string = ''
53
+ if self.new.attributes.keys.dup.push("popularity").include?(by)
54
+ order_string << by
55
+ else
56
+ raise TagError.new("`by' attribute of `each' tag must be set to a valid field name")
57
+ end
58
+ if order =~ /^(asc|desc)$/i
59
+ order_string << " #{$1.upcase}"
60
+ else
61
+ raise TagError.new(%{`order' attribute of `each' tag must be set to either "asc" or "desc"})
62
+ end
63
+
45
64
  end
65
+
46
66
  end
47
67
 
48
68
  def <=>(other)
49
69
  # To be able to sort an array of tags
50
70
  name <=> other.name
51
71
  end
52
-
72
+
53
73
  end
@@ -85,7 +85,7 @@ module RadiusTags
85
85
  The results_page attribute will default to #{Radiant::Config['tags.results_page_url']}
86
86
 
87
87
  *Usage:*
88
- <pre><code><r:tag_cloud_list [limit="number"] [results_page="/some/url"] [scope="/some/url"]/></code></pre>
88
+ <pre><code><r:tag_cloud [limit="number"] [results_page="/some/url"] [scope="/some/url"]/></code></pre>
89
89
  }
90
90
  tag "tag_cloud" do |tag|
91
91
  tag_cloud = MetaTag.cloud(:limit => tag.attr['limit'] || 5).sort
@@ -98,10 +98,33 @@ module RadiusTags
98
98
  output += "<li class=\"#{cloud_class}\"><span>#{pluralize(amount, 'page is', 'pages are')} tagged with </span><a href=\"#{results_page}/#{tag}\" class=\"tag\">#{tag}</a></li>"
99
99
  end
100
100
  else
101
- return "<p>No tags found.</p>"
101
+ return I18n.t('tags_extension.no_tags_found')
102
102
  end
103
103
  output += "</ol>"
104
104
  end
105
+
106
+ desc %{
107
+ Render a Tag cloud with div-tags
108
+ The results_page attribute will default to #{Radiant::Config['tags.results_page_url']}
109
+
110
+ *Usage:*
111
+ <pre><code><r:tag_cloud_div [limit="number"] [results_page="/some/url"] [scope="/some/url"]/></code></pre>
112
+ }
113
+ tag "tag_cloud_div" do |tag|
114
+ tag_cloud = MetaTag.cloud(:limit => tag.attr['limit'] || 10).sort
115
+ tag_cloud = filter_tags_to_url_scope(tag_cloud, tag.attr['scope']) unless tag.attr['scope'].nil?
116
+
117
+ results_page = tag.attr['results_page'] || Radiant::Config['tags.results_page_url']
118
+ output = "<div class=\"tag_cloud\">"
119
+ if tag_cloud.length > 0
120
+ build_tag_cloud(tag_cloud, %w(size1 size2 size3 size4 size5 size6 size7 size8 size9)) do |tag, cloud_class, amount|
121
+ output += "<div class=\"#{cloud_class}\"><a href=\"#{results_page}/#{tag}\" class=\"tag\">#{tag}</a></div>\n"
122
+ end
123
+ else
124
+ return I18n.t('tags_extension.no_tags_found')
125
+ end
126
+ output += "</div>"
127
+ end
105
128
 
106
129
  desc %{
107
130
  Render a Tag list, more for 'categories'-ish usage, i.e.: Cats (2) Logs (1) ...
@@ -121,7 +144,7 @@ module RadiusTags
121
144
  output += "<li class=\"#{cloud_class}\"><a href=\"#{results_page}/#{tag}\" class=\"tag\">#{tag} (#{amount})</a></li>"
122
145
  end
123
146
  else
124
- return "<p>No tags found.</p>"
147
+ return I18n.t('tags_extension.no_tags_found')
125
148
  end
126
149
  output += "</ul>"
127
150
  end
@@ -174,18 +197,14 @@ module RadiusTags
174
197
  Iterates through each tag and allows you to specify the order: by popularity or by name.
175
198
  The default is by name. You may also limit the search; the default is 5 results.
176
199
 
177
- Usage: <pre><code><r:all_tags:each order="popularity" limit="5">...</r:all_tags:each></code></pre>
200
+ Usage: <pre><code><r:all_tags:each [order="asc|desc"] [by="name|popularity"] limit="5">...</r:all_tags:each></code></pre>
178
201
  }
179
202
  tag "all_tags:each" do |tag|
180
- order = tag.attr['order'] || 'name'
203
+ by = tag.attr['by'] || 'name'
204
+ order = tag.attr['order'] || 'asc'
181
205
  limit = tag.attr['limit'] || '5'
182
206
  result = []
183
- case order
184
- when 'name'
185
- all_tags = MetaTag.find(:all, :limit => limit)
186
- else
187
- all_tags = MetaTag.cloud(:limit => limit)
188
- end
207
+ all_tags = MetaTag.cloud(:limit => limit, :order => order, :by => by)
189
208
  all_tags.each do |t|
190
209
  next if t.pages.empty? # skip unused tags
191
210
  tag.locals.meta_tag = t
@@ -202,9 +221,18 @@ module RadiusTags
202
221
  tag "all_tags:each:link" do |tag|
203
222
  results_page = tag.attr['results_page'] || Radiant::Config['tags.results_page_url']
204
223
  name = tag.locals.meta_tag.name
205
- return "<a href=\"#{results_page}/#{name}\" class=\"tag\">#{name}</a>"
224
+ "<a href=\"#{results_page}/#{name}\" class=\"tag\">#{name}</a>"
225
+ end
226
+
227
+ tag "all_tags:each:popularity" do |tag|
228
+ (tag.locals.meta_tag.respond_to?(:popularity)) ? tag.locals.meta_tag.popularity : ""
229
+ end
230
+
231
+ tag "all_tags:each:url" do |tag|
232
+ results_page = tag.attr['results_page'] || Radiant::Config['tags.results_page_url']
233
+ name = tag.locals.meta_tag.name
234
+ "#{results_page}/#{name}"
206
235
  end
207
-
208
236
 
209
237
  desc "Set the scope for the tag's pages"
210
238
  tag "all_tags:each:pages" do |tag|
@@ -1,7 +1,28 @@
1
1
  class TagSearchPage < Page
2
2
 
3
3
  attr_accessor :requested_tag
4
+
4
5
  #### Tags ####
6
+
7
+ desc %{ The namespace for all tagsearch tags.}
8
+ tag 'tagsearch' do |tag|
9
+ tag.expand
10
+ end
11
+
12
+ desc %{ to show a index page, if the page was not able to find a tag, it is considered to be a index page}
13
+ tag 'tagsearch:if_index' do |tag|
14
+ if requested_tag.nil? || (requested_tag && requested_tag.blank?)
15
+ tag.expand
16
+ end
17
+ end
18
+
19
+ desc %{ resultpage, if the page got a tag, it is considered to be a result page}
20
+ tag 'tagsearch:unless_index' do |tag|
21
+ if requested_tag && ! requested_tag.blank?
22
+ tag.expand
23
+ end
24
+ end
25
+
5
26
  desc %{ The namespace for all search tags.}
6
27
  tag 'search' do |tag|
7
28
  tag.expand
@@ -68,8 +89,8 @@ class TagSearchPage < Page
68
89
  end
69
90
 
70
91
  def render
71
- self.requested_tag = @request.parameters[:tag] unless requested_tag
72
- self.title = "Tagged with #{requested_tag}" if requested_tag
92
+ self.requested_tag = @request.parameters[:tag] if @request.parameters[:tag]
93
+ self.title = "#{self.title} #{requested_tag}" if requested_tag
73
94
 
74
95
  super
75
96
  end
@@ -1,8 +1,14 @@
1
- %tr
1
+ = render 'tag_field_javascript' if Radiant::Config['tags.complex_strings'] == 'true'
2
+
3
+ %tr.tags
2
4
  %th.label
3
5
  %label{:for => "page_meta_tags"}
4
- Tags
6
+ = t('tags_extension.tags')
5
7
  %td.field
6
8
  = f.text_field :meta_tags, :value => @page.tag_list, :class => 'textbox'
9
+ #tag_container{:style => "display: none"}
10
+ .default
11
+ = t('tags_extension.type_a_tag')
12
+ %ul
7
13
  - unless model.errors.on_base.nil?
8
14
  %span.error= model.errors.on_base
@@ -0,0 +1,17 @@
1
+ <%
2
+ include_javascript 'admin/protomultiselect.js'
3
+ include_stylesheet 'admin/tags.css'
4
+ %>
5
+
6
+ <% content_for 'page_scripts' do %>
7
+ document.observe('dom:loaded', function() {
8
+ var taglist = new FacebookList('page_meta_tags', 'tag_container', {
9
+ newValues: true,
10
+ separator: ';',
11
+ separatorKeyCode: 186
12
+ })
13
+
14
+ var tags = <%= MetaTag.all.map {|tag| {:caption => tag.name, :value => tag.name} }.to_json %>
15
+ tags.each(function(t) { taglist.autoFeed(t) })
16
+ });
17
+ <% end %>
@@ -0,0 +1,7 @@
1
+ ---
2
+ de:
3
+ tags_extension:
4
+ no_tags_found: "<p>Es wurden keine Schlagworte gefunden.</p>"
5
+ tags: "Schlagworte"
6
+ type_a_tag: Separate tags with ;
7
+
@@ -0,0 +1,5 @@
1
+ en:
2
+ tags_extensions:
3
+ no_tags_found: "<p>No tags found.</p>"
4
+ tags: Tags
5
+ type_a_tag: Separate tags with ;
@@ -0,0 +1,5 @@
1
+ nl:
2
+ tags_extension:
3
+ no_tags_found: "<p>Geen tags gevonden.</p>"
4
+ tags: Tags
5
+ type_a_tag: Onderscheid tags met ;
data/config/routes.rb ADDED
@@ -0,0 +1,20 @@
1
+ ActionController::Routing::Routes.draw do |map|
2
+ if Radiant::Config['tags.results_page_url'].blank?
3
+ Radiant::Config['tags.results_page_url'] = TagsExtension::DEFAULT_RESULTS_URL if Radiant::Config['tags.results_page_url'].blank?
4
+ end
5
+ begin
6
+ if defined?(SiteLanguage) && SiteLanguage.count > 0
7
+ include Globalize
8
+ SiteLanguage.codes.each do |code|
9
+ langname = Locale.new(code).language.code
10
+ map.connect "#{langname}#{Radiant::Config['tags.results_page_url']}/:tag", :controller => 'site', :action => 'show_page', :url => Radiant::Config['tags.results_page_url'], :language => code
11
+ end
12
+ else if defined?(VhostExtension)
13
+ map.connect "#{Radiant::Config['tags.results_page_url']}/:tag", :controller => 'site', :action => 'show_page', :url => Radiant::Config['tags.results_page_url']
14
+ end
15
+ map.connect "#{Radiant::Config['tags.results_page_url']}/:tag", :controller => 'site', :action => 'show_page', :url => Radiant::Config['tags.results_page_url']
16
+ end
17
+ rescue
18
+ # dirty hack; need to get trough here to allow migrations to run..
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module RadiantTagsExtension
2
+ VERSION = '1.6.0'
3
+ end
@@ -58,9 +58,7 @@ TaggingMethods = Proc.new do
58
58
  sql << "AND taggings.meta_tag_id = meta_tags.id "
59
59
  sql << "AND pages.site_id = #{current_site.id} " if self.respond_to?(:current_site)
60
60
 
61
- tag_list_condition = tag_list.map {|name| "\"#{name.strip.squeeze(' ')}\""}.join(", ")
62
-
63
- sql << "AND (meta_tags.name IN (#{sanitize_sql(tag_list_condition)})) "
61
+ sql << "AND (#{sanitize_sql(['meta_tags.name IN (?)', tag_list.map{|name| name.strip.squeeze(' ')}])}) "
64
62
  sql << "AND #{sanitize_sql(options[:conditions])} " if options[:conditions]
65
63
 
66
64
  columns = column_names.map do |column|
Binary file
@@ -0,0 +1,527 @@
1
+ /*
2
+ Proto!MultiSelect
3
+ Copyright: InteRiders <http://interiders.com/> - Distributed under MIT - Keep this message!
4
+ */
5
+
6
+ // Added key contstant for COMMA watching happiness
7
+ Object.extend(Event, { KEY_COMMA: 188, KEY_SPACE: 32 });
8
+
9
+ var ResizableTextbox = Class.create({
10
+ initialize: function(element, options) {
11
+ var that = this;
12
+ this.options = $H({
13
+ min: 5,
14
+ max: 500,
15
+ step: 7
16
+ });
17
+ this.options.update(options);
18
+ this.el = $(element);
19
+ this.width = this.el.offsetWidth;
20
+ this.el.observe(
21
+ 'keyup', function() {
22
+ var newsize = that.options.get('step') * $F(this).length;
23
+ if(newsize <= that.options.get('min')) newsize = that.options.get('mix');
24
+ if(! ($F(this).length == this.retrieveData('rt-value') || newsize <= that.options.min || newsize >= that.options.max))
25
+ this.setStyle({'width': newsize});
26
+ }).observe('keydown', function() {
27
+ this.cacheData('rt-value', $F(this).length);
28
+ }
29
+ );
30
+ }
31
+ });
32
+
33
+ var TextboxList = Class.create({
34
+ initialize: function(element, options) {
35
+ this.options = $H({/*
36
+ onFocus: $empty,
37
+ onBlur: $empty,
38
+ onInputFocus: $empty,
39
+ onInputBlur: $empty,
40
+ onBoxFocus: $empty,
41
+ onBoxBlur: $empty,
42
+ onBoxDispose: $empty,*/
43
+ resizable: {},
44
+ className: 'bit',
45
+ separator: ',',
46
+ separatorKeyCode: Event.KEY_COMMA,
47
+ extrainputs: true,
48
+ startinput: true,
49
+ hideempty: true,
50
+ newValues: false,
51
+ newValueDelimiters: ['[',']'],
52
+ spaceReplace: '',
53
+ fetchFile: undefined,
54
+ fetchMethod: 'get',
55
+ results: 10,
56
+ maxResults: 0, // 0 = set to default (which is 10 (see FacebookList class)),
57
+ wordMatch: false,
58
+ onEmptyInput: function(input){},
59
+ caseSensitive: false,
60
+ regexSearch: true
61
+ });
62
+ this.current_input = "";
63
+ this.options.update(options);
64
+ this.element = $(element).hide();
65
+ this.bits = new Hash();
66
+ this.events = new Hash();
67
+ this.count = 0;
68
+ this.current = false;
69
+ this.maininput = this.createInput({'class': 'maininput'});
70
+ this.holder = new Element('ul', {
71
+ 'class': 'holder'
72
+ }).insert(this.maininput);
73
+ this.element.insert({'before':this.holder});
74
+ this.holder.observe('click', function(event){
75
+ event.stop();
76
+ if(this.maininput != this.current) this.focus(this.maininput);
77
+ }.bind(this));
78
+ this.makeResizable(this.maininput);
79
+ this.setEvents();
80
+ },
81
+
82
+ setEvents: function() {
83
+ document.observe(Prototype.Browser.IE ? 'keydown' : 'keypress', function(e) {
84
+ if(! this.current) return;
85
+ if(this.current.retrieveData('type') == 'box' && e.keyCode == Event.KEY_BACKSPACE) e.stop();
86
+ }.bind(this));
87
+
88
+ document.observe(
89
+ 'keyup', function(e) {
90
+ e.stop();
91
+ if(! this.current) return;
92
+ switch(e.keyCode){
93
+ case Event.KEY_LEFT: return this.move('left');
94
+ case Event.KEY_RIGHT: return this.move('right');
95
+ case Event.KEY_DELETE:
96
+ case Event.KEY_BACKSPACE: return this.moveDispose();
97
+ }
98
+ }.bind(this)).observe(
99
+ 'click', function() { document.fire('blur'); }.bindAsEventListener(this)
100
+ );
101
+ },
102
+
103
+ update: function() {
104
+ this.element.value = this.bits.values().join(this.options.get('separator'));
105
+ if (!this.current_input.blank()){
106
+ this.element.value += (this.element.value.blank() ? "" : this.options.get('separator')) + this.current_input;
107
+ }
108
+ return this;
109
+ },
110
+
111
+ add: function(text, html) {
112
+ var id = this.id_base + '-' + this.count++;
113
+ var el = this.createBox($pick(html, text), {'id': id, 'class': this.options.get('className'), 'newValue' : text.newValue ? 'true' : 'false'});
114
+ (this.current || this.maininput).insert({'before':el});
115
+ el.observe('click', function(e) {
116
+ e.stop();
117
+ this.focus(el);
118
+ }.bind(this));
119
+ this.bits.set(id, text.value);
120
+ // Dynamic updating... why not?
121
+ this.update();
122
+ if(this.options.get('extrainputs') && (this.options.get('startinput') || el.previous())) this.addSmallInput(el,'before');
123
+ return el;
124
+ },
125
+
126
+ addSmallInput: function(el, where) {
127
+ var input = this.createInput({'class': 'smallinput'});
128
+ el.insert({}[where] = input);
129
+ input.cacheData('small', true);
130
+ this.makeResizable(input);
131
+ if(this.options.get('hideempty')) input.hide();
132
+ return input;
133
+ },
134
+
135
+ dispose: function(el) {
136
+ this.bits.unset(el.id);
137
+ // Dynamic updating... why not?
138
+ this.update();
139
+ if(el.previous() && el.previous().retrieveData('small')) el.previous().remove();
140
+ if(this.current == el) this.focus(el.next());
141
+ if(el.retrieveData('type') == 'box') el.onBoxDispose(this);
142
+ el.remove();
143
+ return this;
144
+ },
145
+
146
+ focus: function(el, nofocus) {
147
+ if(! this.current) el.fire('focus');
148
+ else if(this.current == el) return this;
149
+ this.blur();
150
+ el.addClassName(this.options.get('className') + '-' + el.retrieveData('type') + '-focus');
151
+ if(el.retrieveData('small')) el.setStyle({'display': 'block'});
152
+ if(el.retrieveData('type') == 'input') {
153
+ el.onInputFocus(this);
154
+ if(! nofocus) this.callEvent(el.retrieveData('input'), 'focus');
155
+ }
156
+ else el.fire('onBoxFocus');
157
+ this.current = el;
158
+ return this;
159
+ },
160
+
161
+ blur: function(noblur) {
162
+ if(! this.current) return this;
163
+ if(this.current.retrieveData('type') == 'input') {
164
+ var input = this.current.retrieveData('input');
165
+ if(! noblur) this.callEvent(input, 'blur');
166
+ input.onInputBlur(this);
167
+ }
168
+ else this.current.fire('onBoxBlur');
169
+ if(this.current.retrieveData('small') && ! input.get('value') && this.options.get('hideempty'))
170
+ this.current.hide();
171
+ this.current.removeClassName(this.options.get('className') + '-' + this.current.retrieveData('type') + '-focus');
172
+ this.current = false;
173
+ return this;
174
+ },
175
+
176
+ createBox: function(text, options) {
177
+ return new Element('li', options).addClassName(this.options.get('className') + '-box').update(text.caption).cacheData('type', 'box');
178
+ },
179
+
180
+ createInput: function(options) {
181
+ var li = new Element('li', {'class': this.options.get('className') + '-input'});
182
+ var el = new Element('input', Object.extend(options,{'type': 'text', 'autocomplete':'off'}));
183
+ el.observe('click', function(e) { e.stop(); }).observe('focus', function(e) { if(! this.isSelfEvent('focus')) this.focus(li, true); }.bind(this)).observe('blur', function() { if(! this.isSelfEvent('blur')) this.blur(true); }.bind(this)).observe('keydown', function(e) { this.cacheData('lastvalue', this.value).cacheData('lastcaret', this.getCaretPosition()); });
184
+ var tmp = li.cacheData('type', 'input').cacheData('input', el).insert(el);
185
+ return tmp;
186
+ },
187
+
188
+ callEvent: function(el, type) {
189
+ this.events.set(type, el);
190
+ el[type]();
191
+ },
192
+
193
+ isSelfEvent: function(type) {
194
+ return (this.events.get(type)) ? !! this.events.unset(type) : false;
195
+ },
196
+
197
+ makeResizable: function(li) {
198
+ var el = li.retrieveData('input');
199
+ el.cacheData('resizable', new ResizableTextbox(el, Object.extend(this.options.get('resizable'),{min: 150, max: 500})));
200
+ return this;
201
+ },
202
+
203
+ checkInput: function() {
204
+ var input = this.current.retrieveData('input');
205
+ return (! input.retrieveData('lastvalue') || (input.getCaretPosition() === 0 && input.retrieveData('lastcaret') === 0));
206
+ },
207
+
208
+ move: function(direction) {
209
+ var el = this.current[(direction == 'left' ? 'previous' : 'next')]();
210
+ if(el && (! this.current.retrieveData('input') || ((this.checkInput() || direction == 'right')))) this.focus(el);
211
+ return this;
212
+ },
213
+
214
+ moveDispose: function() {
215
+ if(this.current.retrieveData('type') == 'box') return this.dispose(this.current);
216
+ if(this.checkInput() && this.bits.keys().length && this.current.previous()) return this.focus(this.current.previous());
217
+ }
218
+ });
219
+
220
+ //helper functions
221
+ Element.addMethods({
222
+ getCaretPosition: function() {
223
+ if (this.createTextRange) {
224
+ var r = document.selection.createRange().duplicate();
225
+ r.moveEnd('character', this.value.length);
226
+ if (r.text === '') return this.value.length;
227
+ return this.value.lastIndexOf(r.text);
228
+ } else return this.selectionStart;
229
+ },
230
+ cacheData: function(element, key, value) {
231
+ if (Object.isUndefined(this[$(element).identify()]) || !Object.isHash(this[$(element).identify()]))
232
+ this[$(element).identify()] = $H();
233
+ this[$(element).identify()].set(key,value);
234
+ return element;
235
+ },
236
+ retrieveData: function(element,key) {
237
+ return this[$(element).identify()].get(key);
238
+ }
239
+ });
240
+
241
+ function $pick(){for(var B=0,A=arguments.length;B<A;B++){if(!Object.isUndefined(arguments[B])){return arguments[B];}}return null;}
242
+
243
+ var FacebookList = Class.create(TextboxList, {
244
+ initialize: function($super,element, autoholder, options, func) {
245
+ $super(element, options);
246
+ this.loptions = $H({
247
+ autocomplete: {
248
+ 'opacity': 1,
249
+ 'maxresults': 10,
250
+ 'minchars': 1
251
+ }
252
+ });
253
+
254
+ this.id_base = $(element).identify() + "_" + this.options.get("className");
255
+
256
+ this.data = [];
257
+ this.data_searchable = [];
258
+ this.autoholder = $(autoholder).setOpacity(this.loptions.get('autocomplete').opacity);
259
+ this.autoholder.observe('mouseover',function() {this.curOn = true;}.bind(this)).observe('mouseout',function() {this.curOn = false;}.bind(this));
260
+ this.autoresults = this.autoholder.select('ul').first();
261
+ var children = this.autoresults.select('li');
262
+ children.each(function(el) { this.add({value:el.readAttribute('value'),caption:el.innerHTML}); }, this);
263
+
264
+ $F(this.element).split(this.options.get('separator')).each(function(item) {
265
+ this.add({value:item,caption:item});
266
+ }, this)
267
+
268
+ // Loading the options list only once at initialize.
269
+ // This would need to be further extended if the list was exceptionally long
270
+ if (!Object.isUndefined(this.options.get('fetchFile'))) {
271
+ new Ajax.Request(this.options.get('fetchFile'), {
272
+ method: this.options.get('fetchMethod'),
273
+ onSuccess: function(transport) {
274
+ transport.responseText.evalJSON(true).each(function(t) {
275
+ this.autoFeed(t) }.bind(this));
276
+ }.bind(this)
277
+ }
278
+ );
279
+ }
280
+ },
281
+
282
+ autoShow: function(search) {
283
+ this.autoholder.setStyle({'display': 'block'});
284
+ this.autoholder.descendants().each(function(e) { e.hide() });
285
+ if(! search || ! search.strip() || (! search.length || search.length < this.loptions.get('autocomplete').minchars)) {
286
+ this.autoholder.select('.default').first().setStyle({'display': 'block'});
287
+ this.resultsshown = false;
288
+ } else {
289
+ this.resultsshown = true;
290
+ this.autoresults.setStyle({'display': 'block'}).update('');
291
+ if (!this.options.get('regexSearch')) {
292
+ var matches = new Array();
293
+ if (search) {
294
+ if (!this.options.get('caseSensitive')) {
295
+ search = search.toLowerCase();
296
+ }
297
+ var matches_found = 0;
298
+ for (var i=0,len=this.data_searchable.length; i<len; i++) {
299
+ if (this.data_searchable[i].indexOf(search) >= 0) {
300
+ matches[matches_found++] = this.data[i];
301
+ }
302
+ }
303
+ }
304
+ } else {
305
+ if (this.options.get('wordMatch')) {
306
+ var regexp = new RegExp("(^|\\s)"+search,(!this.options.get('caseSensitive') ? 'i' : ''));
307
+ } else {
308
+ var regexp = new RegExp(search,(!this.options.get('caseSensitive') ? 'i' : ''));
309
+ var matches = this.data.filter(
310
+ function(str) {
311
+ return str ? regexp.test(str.evalJSON(true).caption) : false;
312
+ });
313
+ }
314
+ }
315
+ var count = 0;
316
+ matches.each(
317
+ function(result, ti) {
318
+ count++;
319
+ if(ti >= (this.options.get('maxResults') ? this.options.get('maxResults') : this.loptions.get('autocomplete').maxresults)) return;
320
+ var that = this;
321
+ var el = new Element('li');
322
+ el.observe('click',function(e) {
323
+ e.stop();
324
+ that.current_input = "";
325
+ that.autoAdd(this);
326
+ }
327
+ ).observe('mouseover', function() { that.autoFocus(this); } ).update(
328
+ this.autoHighlight(result.evalJSON(true).caption, search)
329
+ );
330
+ this.autoresults.insert(el);
331
+ el.cacheData('result', result.evalJSON(true));
332
+ if(ti == 0) this.autoFocus(el);
333
+ },
334
+ this
335
+ );
336
+ }
337
+ if (count == 0) {
338
+ // if there are no results, hide everything so that KEY_ENTER has no effect
339
+ this.autoHide();
340
+ } else {
341
+ if (count > this.options.get('results'))
342
+ this.autoresults.setStyle({'height': (this.options.get('results')*24)+'px'});
343
+ else
344
+ this.autoresults.setStyle({'height': (count?(count*24):0)+'px'});
345
+ }
346
+
347
+ return this;
348
+ },
349
+
350
+ autoHighlight: function(html, highlight) {
351
+ return html.gsub(new RegExp(highlight,'i'), function(match) {
352
+ return '<em>' + match[0] + '</em>';
353
+ });
354
+ },
355
+
356
+ autoHide: function() {
357
+ this.resultsshown = false;
358
+ this.autoholder.hide();
359
+ return this;
360
+ },
361
+
362
+ autoFocus: function(el) {
363
+ if(! el) return;
364
+ if(this.autocurrent) this.autocurrent.removeClassName('auto-focus');
365
+ this.autocurrent = el.addClassName('auto-focus');
366
+ return this;
367
+ },
368
+
369
+ autoMove: function(direction) {
370
+ if(!this.resultsshown) return;
371
+ this.autoFocus(this.autocurrent[(direction == 'up' ? 'previous' : 'next')]());
372
+ this.autoresults.scrollTop = this.autocurrent.positionedOffset()[1]-this.autocurrent.getHeight();
373
+ return this;
374
+ },
375
+
376
+ autoFeed: function(text) {
377
+ var with_case = this.options.get('caseSensitive');
378
+ if (this.data.indexOf(Object.toJSON(text)) == -1) {
379
+ this.data.push(Object.toJSON(text));
380
+ this.data_searchable.push(with_case ? Object.toJSON(text).evalJSON(true).caption : Object.toJSON(text).evalJSON(true).caption.toLowerCase());
381
+ }
382
+ return this;
383
+ },
384
+
385
+ autoAdd: function(el) {
386
+ if(this.newvalue && this.options.get("newValues")) {
387
+ this.add({caption: el.value, value: el.value, newValue: true});
388
+ var input = el;
389
+ } else if(!el || ! el.retrieveData('result')) {
390
+ return;
391
+ } else {
392
+ this.add(el.retrieveData('result'));
393
+ delete this.data[this.data.indexOf(Object.toJSON(el.retrieveData('result')))];
394
+ var input = this.lastinput || this.current.retrieveData('input');
395
+ }
396
+ this.autoHide();
397
+ input.clear().focus();
398
+ return this;
399
+ },
400
+
401
+ createInput: function($super,options) {
402
+ var li = $super(options);
403
+ var input = li.retrieveData('input');
404
+ input.observe('keydown', function(e) {
405
+ this.dosearch = false;
406
+ this.newvalue = false;
407
+
408
+ switch(e.keyCode) {
409
+ case Event.KEY_UP: e.stop(); return this.autoMove('up');
410
+ case Event.KEY_DOWN: e.stop(); return this.autoMove('down');
411
+
412
+ case Event.KEY_RETURN:
413
+ // If the text input is blank and the user hits Enter call the
414
+ // onEmptyInput callback.
415
+ if (String('').valueOf() == String(this.current.retrieveData('input').getValue()).valueOf()) {
416
+ this.options.get("onEmptyInput")();
417
+ }
418
+ e.stop();
419
+ if(!this.autocurrent || !this.resultsshown) break;
420
+ this.current_input = "";
421
+ this.autoAdd(this.autocurrent);
422
+ this.autocurrent = false;
423
+ this.autoenter = true;
424
+ break;
425
+ case Event.KEY_ESC:
426
+ this.autoHide();
427
+ if(this.current && this.current.retrieveData('input'))
428
+ this.current.retrieveData('input').clear();
429
+ break;
430
+ default:
431
+ this.dosearch = true;
432
+ }
433
+ }.bind(this));
434
+ input.observe('keyup',function(e) {
435
+ switch(e.keyCode) {
436
+ case this.options.get('separatorKeyCode'):
437
+ if(this.options.get('newValues')) {
438
+ var separator = this.options.get('separator');
439
+ new_value_el = this.current.retrieveData('input');
440
+ if (!new_value_el.value.endsWith('<')) {
441
+ keep_input = "";
442
+ new_value_el.value = new_value_el.value.strip();
443
+ if (new_value_el.value.indexOf(separator) < (new_value_el.value.length - 1)){
444
+ comma_pos = new_value_el.value.indexOf(separator);
445
+ keep_input = new_value_el.value.substr(comma_pos + 1);
446
+ new_value_el.value = new_value_el.value.substr(0,comma_pos).escapeHTML().strip();
447
+ } else {
448
+ new_value_el.value = new_value_el.value.gsub(separator,"").escapeHTML().strip();
449
+ }
450
+ if(!this.options.get("spaceReplace").blank()) new_value_el.value.gsub(" ", this.options.get("spaceReplace"));
451
+ if(!new_value_el.value.blank()) {
452
+ e.stop();
453
+ this.newvalue = true;
454
+ this.current_input = keep_input.escapeHTML().strip();
455
+ this.autoAdd(new_value_el);
456
+ input.value = keep_input;
457
+ this.update();
458
+ }
459
+ }
460
+ }
461
+ break;
462
+ case Event.KEY_UP:
463
+ case Event.KEY_DOWN:
464
+ case Event.KEY_RETURN:
465
+ case Event.KEY_ESC:
466
+ break;
467
+ default:
468
+ // If the user doesn't add comma after, the value is discarded upon submit
469
+ this.current_input = input.value.strip().escapeHTML();
470
+ this.update();
471
+
472
+ // Removed Ajax.Request from here and moved to initialize,
473
+ // now doesn't create server queries every search but only
474
+ // refreshes the list on initialize (page load)
475
+ if(this.searchTimeout) clearTimeout(this.searchTimeout);
476
+ this.searchTimeout = setTimeout(function(){
477
+ var sanitizer = new RegExp("[({[^$*+?\\\]})]","g");
478
+ if(this.dosearch) this.autoShow(input.value.replace(sanitizer,"\\$1"));
479
+ }.bind(this), 250);
480
+ }
481
+ }.bind(this));
482
+ input.observe(Prototype.Browser.IE ? 'keydown' : 'keypress', function(e) {
483
+ if ((e.keyCode == Event.KEY_RETURN) && this.autoenter) e.stop();
484
+ this.autoenter = false;
485
+ }.bind(this));
486
+ return li;
487
+ },
488
+
489
+ createBox: function($super,text, options) {
490
+ var li = $super(text, options);
491
+ li.observe('mouseover',function() {
492
+ this.addClassName('bit-hover');
493
+ }).observe('mouseout',function() {
494
+ this.removeClassName('bit-hover')
495
+ });
496
+ var a = new Element('a', {
497
+ 'href': '#',
498
+ 'class': 'closebutton'
499
+ });
500
+ a.observe('click',function(e) {
501
+ e.stop();
502
+ if(! this.current) this.focus(this.maininput);
503
+ this.dispose(li);
504
+ }.bind(this));
505
+ li.insert(a).cacheData('text', Object.toJSON(text));
506
+ return li;
507
+ }
508
+ });
509
+
510
+ Element.addMethods({
511
+ onBoxDispose: function(item,obj) {
512
+ // Set to not to "add back" values in the drop-down upon delete if they were new values
513
+ item = item.retrieveData('text').evalJSON(true);
514
+ if(!item.newValue)
515
+ obj.autoFeed(item);
516
+ },
517
+ onInputFocus: function(el,obj) { obj.autoShow(); },
518
+ onInputBlur: function(el,obj) {
519
+ obj.lastinput = el;
520
+ if(!obj.curOn) {
521
+ obj.blurhide = obj.autoHide.bind(obj).delay(0.1);
522
+ }
523
+ },
524
+ filter: function(D,E) { var C=[];for(var B=0,A=this.length;B<A;B++){if(D.call(E,this[B],B,this)){C.push(this[B]);}} return C; }
525
+ });
526
+
527
+ /* Copyright: InteRiders <http://interiders.com/> - Distributed under MIT - Keep this message! */
@@ -0,0 +1,21 @@
1
+ tr.tags ul.holder { width: 99.5%; margin: 0; border: 1px solid #999; overflow: hidden; height: auto !important; height: 1%; padding: 4px 5px 0; background-color: white; display: block !important; }
2
+ *:first-child+html ul.holder { padding-bottom: 2px; } * html ul.holder { padding-bottom: 2px; } /* ie7 and below */
3
+ tr.tags ul.holder li { float: left; list-style-type: none; margin: 0 5px 4px 0;}
4
+ tr.tags ul.holder li.bit-box { -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px; border: 1px solid #CAD8F3; background: #DEE7F8; padding: 1px 5px 2px; }
5
+ tr.tags ul.holder li.bit-box-focus { border-color: #598BEC; background: #598BEC; color: #fff; }
6
+ tr.tags ul.holder li.bit-input input { width: 150px; margin: 0; border: none; outline: 0; padding: 3px 0 2px; } /* no left/right padding here please */
7
+ tr.tags ul.holder li.bit-box { padding-right: 15px !important; position: relative; color: #000 !important;}
8
+ tr.tags ul.holder li.bit-input { margin: 0;}
9
+
10
+ tr.tags ul.holder li.bit-box a.closebutton { position: absolute; right: 4px; top: 5px; display: block; width: 7px; height: 7px; font-size: 1px; background: url('/images/admin/tagclose.gif'); }
11
+ tr.tags ul.holder li.bit-box a.closebutton:hover { background-position: 7px; }
12
+ tr.tags ul.holder li.bit-box-focus a.closebutton, tr.tags ul.holder li.bit-box-focus a.closebutton:hover { background-position: bottom; }
13
+
14
+
15
+ #tag_container { display: none; position: relative; background: #eee; width: 101%; }
16
+ #tag_container .default { padding: 5px 7px; border: 1px solid #ccc; border-width: 0 1px 1px; color: black; text-align: left; }
17
+ #tag_container ul { display: none; margin: 0; padding: 0; overflow: auto }
18
+ #tag_container ul li { display: block; padding: 5px 12px; z-index: 1000; cursor: pointer; margin: 0; list-style-type: none; border: 1px solid #ccc; border-width: 0 1px 1px; font: 11px "Lucida Grande", "Verdana"; text-align: left; color: black;}
19
+ #tag_container ul li em { font-weight: bold; font-style: normal; background: #ccc; }
20
+ #tag_container ul li.auto-focus { background: #4173CC; color: #fff; }
21
+ #tag_container ul li.auto-focus em { background: none; }
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'radiant-tags-extension'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'radiant-tags-extension'
7
+ s.version = RadiantTagsExtension::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ['Benny Degezelle']
10
+ s.email = ['benny@gorilla-webdesign.be']
11
+ s.homepage = 'http://ext.radiantcms.org/extensions/195-tags'
12
+ s.summary = %q{Tagging for Radiant CMS}
13
+ s.description = %q{This extension enhances the page model with tagging capabilities, tagging as in \"2.0" and tagclouds.}
14
+
15
+ # TODO: add gem dependency on this instead of bundling it
16
+ # s.add_dependency 'has_many_polymorphs'
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features,vendor/plugins/*/test,vendor/plugins/*/spec,vendor/plugins/*/features}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+
23
+ # TODO: update for usage with Bundler/Gemfile once Radiant gets that capability
24
+ s.post_install_message = %{
25
+ Add this to your radiant project by adding the following line to your environment.rb:
26
+ config.gem 'radiant-tags-extension', :version => '#{RadiantTagsExtension::VERSION}'
27
+ }
28
+ end
data/tags_extension.rb CHANGED
@@ -9,27 +9,6 @@ class TagsExtension < Radiant::Extension
9
9
 
10
10
  DEFAULT_RESULTS_URL = '/search/by-tag'
11
11
 
12
- define_routes do |map|
13
- if Radiant::Config['tags.results_page_url'].blank?
14
- Radiant::Config['tags.results_page_url'] = TagsExtension::DEFAULT_RESULTS_URL if Radiant::Config['tags.results_page_url'].blank?
15
- end
16
- begin
17
- if defined?(SiteLanguage) && SiteLanguage.count > 0
18
- include Globalize
19
- SiteLanguage.codes.each do |code|
20
- langname = Locale.new(code).language.code
21
- map.connect "#{langname}#{Radiant::Config['tags.results_page_url']}/:tag", :controller => 'site', :action => 'show_page', :url => Radiant::Config['tags.results_page_url'], :language => code
22
- end
23
- else if defined?(VhostExtension)
24
- map.connect "#{Radiant::Config['tags.results_page_url']}/:tag", :controller => 'site', :action => 'show_page', :url => Radiant::Config['tags.results_page_url']
25
- end
26
- map.connect "#{Radiant::Config['tags.results_page_url']}/:tag", :controller => 'site', :action => 'show_page', :url => Radiant::Config['tags.results_page_url']
27
- end
28
- rescue
29
- # dirty hack; need to get trough here to allow migrations to run..
30
- end
31
- end
32
-
33
12
  def activate
34
13
  raise "The Shards extension is required and must be loaded first!" unless defined?(admin.page)
35
14
  if Radiant::Config.table_exists?
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: radiant-tags-extension
3
3
  version: !ruby/object:Gem::Version
4
- hash: 1
4
+ hash: 15
5
5
  prerelease: false
6
6
  segments:
7
7
  - 1
8
- - 5
9
- - 1
10
- version: 1.5.1
8
+ - 6
9
+ - 0
10
+ version: 1.6.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Benny Degezelle
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-03-19 00:00:00 +00:00
18
+ date: 2011-03-29 00:00:00 +02:00
19
19
  default_executable:
20
20
  dependencies: []
21
21
 
@@ -39,10 +39,20 @@ files:
39
39
  - app/models/tagging.rb
40
40
  - app/views/admin/help/_using_tags.html.haml
41
41
  - app/views/admin/pages/_tag_field.html.haml
42
+ - app/views/admin/pages/_tag_field_javascript.html.erb
43
+ - config/locales/de.yml
44
+ - config/locales/en.yml
45
+ - config/locales/nl.yml
46
+ - config/routes.rb
42
47
  - db/migrate/001_add_tag_support.rb
48
+ - lib/radiant-tags-extension.rb
43
49
  - lib/tagging_methods.rb
44
50
  - lib/tasks/tags_extension_tasks.rake
51
+ - public/images/admin/tagclose.gif
52
+ - public/javascripts/admin/protomultiselect.js
53
+ - public/stylesheets/admin/tags.css
45
54
  - public/stylesheets/tags.css
55
+ - radiant-tags-extension.gemspec
46
56
  - tags_extension.rb
47
57
  - test/fixtures/meta_tags.yml
48
58
  - test/fixtures/page_parts.yml
@@ -232,7 +242,7 @@ has_rdoc: true
232
242
  homepage: http://ext.radiantcms.org/extensions/195-tags
233
243
  licenses: []
234
244
 
235
- post_install_message: "\n Add this to your radiant project with:\n config.gem 'radiant-tags-extension', :version => '1.5.1'\n "
245
+ post_install_message: "\n Add this to your radiant project by adding the following line to your environment.rb:\n config.gem 'radiant-tags-extension', :version => '1.6.0'\n "
236
246
  rdoc_options: []
237
247
 
238
248
  require_paths: