radiant-tags-extension 1.5.1 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
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: