radiant-taggable-extension 1.2.3 → 1.2.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +38 -30
- data/VERSION +1 -1
- data/app/controllers/admin/tags_controller.rb +9 -3
- data/app/helpers/taggable_helper.rb +21 -0
- data/app/models/tag.rb +63 -8
- data/app/views/admin/pages/_edit_title.html.haml +6 -22
- data/app/views/admin/tags/_form.html.haml +34 -20
- data/app/views/admin/tags/cloud.html.haml +0 -7
- data/app/views/admin/tags/edit.html.haml +4 -7
- data/app/views/admin/tags/index.html.haml +14 -9
- data/app/views/admin/tags/new.html.haml +4 -3
- data/config/locales/en.yml +9 -0
- data/db/migrate/20110316210834_structural_tags.rb +17 -0
- data/db/migrate/20110411075109_metaphones.rb +15 -0
- data/lib/taggable_admin_ui.rb +3 -3
- data/lib/taggable_model.rb +15 -6
- data/lib/taggable_page.rb +6 -2
- data/lib/taggable_tags.rb +172 -99
- data/lib/text/double_metaphone.rb +356 -0
- data/lib/text/metaphone.rb +97 -0
- data/public/javascripts/admin/taggable.js +19 -0
- data/public/javascripts/autocomplete.js +334 -0
- data/public/stylesheets/sass/admin/taggable.sass +89 -0
- data/radiant-taggable-extension.gemspec +11 -3
- data/taggable_extension.rb +3 -16
- metadata +13 -5
- data/public/stylesheets/admin/tags.css +0 -20
@@ -0,0 +1,19 @@
|
|
1
|
+
var TagSuggester = Behavior.create({
|
2
|
+
initialize: function() {
|
3
|
+
var textbox = this.element;
|
4
|
+
new Autocomplete(textbox, {
|
5
|
+
serviceUrl: '/admin/tags.json',
|
6
|
+
minChars: 2,
|
7
|
+
maxHeight: 400,
|
8
|
+
deferRequestBy: 500,
|
9
|
+
multiple: true
|
10
|
+
});
|
11
|
+
}
|
12
|
+
});
|
13
|
+
|
14
|
+
Event.addBehavior({
|
15
|
+
'input.toggle': Toggle.CheckboxBehavior({ onLoad: function(link) { if (!this.checked) Toggle.hide(this.toggleWrappers, this.effect); } }),
|
16
|
+
'input.tagger': TagSuggester
|
17
|
+
});
|
18
|
+
|
19
|
+
|
@@ -0,0 +1,334 @@
|
|
1
|
+
/*
|
2
|
+
*
|
3
|
+
* Ajax Autocomplete for Prototype, version 1.0.4
|
4
|
+
* (c) 2010 Tomas Kirda
|
5
|
+
*
|
6
|
+
* Ajax Autocomplete for Prototype is freely distributable under the terms of an MIT-style license.
|
7
|
+
* For details, see the web site: http://www.devbridge.com/projects/autocomplete/
|
8
|
+
*
|
9
|
+
* Adapted by Will 11/4/2011 to take a 'multiple' option and work with a comma-separated list of values.
|
10
|
+
* Type-ahead would be nice too.
|
11
|
+
*/
|
12
|
+
|
13
|
+
var Autocomplete = function(el, options){
|
14
|
+
this.el = $(el);
|
15
|
+
this.id = this.el.identify();
|
16
|
+
this.el.setAttribute('autocomplete','off');
|
17
|
+
this.suggestions = [];
|
18
|
+
this.data = [];
|
19
|
+
this.badQueries = [];
|
20
|
+
this.selectedIndex = -1;
|
21
|
+
this.currentValue = this.el.value;
|
22
|
+
this.intervalId = 0;
|
23
|
+
this.cachedResponse = [];
|
24
|
+
this.instanceId = null;
|
25
|
+
this.onChangeInterval = null;
|
26
|
+
this.ignoreValueChange = false;
|
27
|
+
this.serviceUrl = options.serviceUrl;
|
28
|
+
this.options = {
|
29
|
+
autoSubmit:false,
|
30
|
+
minChars:1,
|
31
|
+
maxHeight:300,
|
32
|
+
deferRequestBy:0,
|
33
|
+
width:0,
|
34
|
+
container:null
|
35
|
+
};
|
36
|
+
if(options){ Object.extend(this.options, options); }
|
37
|
+
if(Autocomplete.isDomLoaded){
|
38
|
+
this.initialize();
|
39
|
+
}else{
|
40
|
+
Event.observe(document, 'dom:loaded', this.initialize.bind(this), false);
|
41
|
+
}
|
42
|
+
};
|
43
|
+
|
44
|
+
Autocomplete.instances = [];
|
45
|
+
Autocomplete.isDomLoaded = false;
|
46
|
+
|
47
|
+
Autocomplete.getInstance = function(id){
|
48
|
+
var instances = Autocomplete.instances;
|
49
|
+
var i = instances.length;
|
50
|
+
while(i--){ if(instances[i].id === id){ return instances[i]; }}
|
51
|
+
};
|
52
|
+
|
53
|
+
Autocomplete.highlight = function(value, re){
|
54
|
+
return value.replace(re, function(match){ return '<strong>' + match + '<\/strong>'; });
|
55
|
+
};
|
56
|
+
|
57
|
+
Autocomplete.prototype = {
|
58
|
+
|
59
|
+
killerFn: null,
|
60
|
+
|
61
|
+
initialize: function() {
|
62
|
+
var me = this;
|
63
|
+
this.killerFn = function(e) {
|
64
|
+
if (!$(Event.element(e)).up('.autocomplete')) {
|
65
|
+
me.killSuggestions();
|
66
|
+
me.disableKillerFn();
|
67
|
+
}
|
68
|
+
} .bindAsEventListener(this);
|
69
|
+
|
70
|
+
if (!this.options.width) { this.options.width = this.el.getWidth(); }
|
71
|
+
|
72
|
+
var div = new Element('div', { style: 'position:absolute;' });
|
73
|
+
div.update('<div class="autocomplete-w1"><div class="autocomplete-w2"><div class="autocomplete" id="Autocomplete_' + this.id + '" style="display:none; width:' + this.options.width + 'px;"></div></div></div>');
|
74
|
+
|
75
|
+
this.options.container = $(this.options.container);
|
76
|
+
if (this.options.container) {
|
77
|
+
this.options.container.appendChild(div);
|
78
|
+
this.fixPosition = function() { };
|
79
|
+
} else {
|
80
|
+
document.body.appendChild(div);
|
81
|
+
}
|
82
|
+
|
83
|
+
this.mainContainerId = div.identify();
|
84
|
+
this.container = $('Autocomplete_' + this.id);
|
85
|
+
this.fixPosition();
|
86
|
+
|
87
|
+
Event.observe(this.el, window.opera ? 'keypress':'keydown', this.onKeyPress.bind(this));
|
88
|
+
Event.observe(this.el, 'keyup', this.onKeyUp.bind(this));
|
89
|
+
Event.observe(this.el, 'blur', this.enableKillerFn.bind(this));
|
90
|
+
Event.observe(this.el, 'focus', this.fixPosition.bind(this));
|
91
|
+
this.container.setStyle({ maxHeight: this.options.maxHeight + 'px' });
|
92
|
+
this.instanceId = Autocomplete.instances.push(this) - 1;
|
93
|
+
},
|
94
|
+
|
95
|
+
fixPosition: function() {
|
96
|
+
var offset = this.el.cumulativeOffset();
|
97
|
+
$(this.mainContainerId).setStyle({ top: (offset.top + this.el.getHeight()) + 'px', left: offset.left + 'px' });
|
98
|
+
},
|
99
|
+
|
100
|
+
enableKillerFn: function() {
|
101
|
+
Event.observe(document.body, 'click', this.killerFn);
|
102
|
+
},
|
103
|
+
|
104
|
+
disableKillerFn: function() {
|
105
|
+
Event.stopObserving(document.body, 'click', this.killerFn);
|
106
|
+
},
|
107
|
+
|
108
|
+
killSuggestions: function() {
|
109
|
+
this.stopKillSuggestions();
|
110
|
+
this.intervalId = window.setInterval(function() { this.hide(); this.stopKillSuggestions(); } .bind(this), 300);
|
111
|
+
},
|
112
|
+
|
113
|
+
stopKillSuggestions: function() {
|
114
|
+
window.clearInterval(this.intervalId);
|
115
|
+
},
|
116
|
+
|
117
|
+
onKeyPress: function(e) {
|
118
|
+
if (!this.enabled) { return; }
|
119
|
+
// return will exit the function
|
120
|
+
// and event will not fire
|
121
|
+
switch (e.keyCode) {
|
122
|
+
case Event.KEY_ESC:
|
123
|
+
this.el.value = this.currentValue;
|
124
|
+
this.hide();
|
125
|
+
break;
|
126
|
+
case Event.KEY_TAB:
|
127
|
+
case Event.KEY_RETURN:
|
128
|
+
if (this.selectedIndex === -1) {
|
129
|
+
this.hide();
|
130
|
+
return;
|
131
|
+
}
|
132
|
+
this.select(this.selectedIndex);
|
133
|
+
if (e.keyCode === Event.KEY_TAB) { return; }
|
134
|
+
break;
|
135
|
+
case Event.KEY_UP:
|
136
|
+
this.moveUp();
|
137
|
+
break;
|
138
|
+
case Event.KEY_DOWN:
|
139
|
+
this.moveDown();
|
140
|
+
break;
|
141
|
+
default:
|
142
|
+
return;
|
143
|
+
}
|
144
|
+
Event.stop(e);
|
145
|
+
},
|
146
|
+
|
147
|
+
onKeyUp: function(e) {
|
148
|
+
switch (e.keyCode) {
|
149
|
+
case Event.KEY_UP:
|
150
|
+
case Event.KEY_DOWN:
|
151
|
+
return;
|
152
|
+
}
|
153
|
+
clearInterval(this.onChangeInterval);
|
154
|
+
if (this.currentValue !== this.el.value) {
|
155
|
+
if (this.options.deferRequestBy > 0) {
|
156
|
+
// Defer lookup in case when value changes very quickly:
|
157
|
+
this.onChangeInterval = setInterval((function() {
|
158
|
+
this.onValueChange();
|
159
|
+
}).bind(this), this.options.deferRequestBy);
|
160
|
+
} else {
|
161
|
+
this.onValueChange();
|
162
|
+
}
|
163
|
+
}
|
164
|
+
},
|
165
|
+
|
166
|
+
onValueChange: function() {
|
167
|
+
clearInterval(this.onChangeInterval);
|
168
|
+
var newValue = this.activeValue();
|
169
|
+
this.selectedIndex = -1;
|
170
|
+
if (this.ignoreValueChange) {
|
171
|
+
this.ignoreValueChange = false;
|
172
|
+
return;
|
173
|
+
}
|
174
|
+
if (newValue === '' || newValue.length < this.options.minChars) {
|
175
|
+
this.hide();
|
176
|
+
} else {
|
177
|
+
this.getSuggestions();
|
178
|
+
}
|
179
|
+
},
|
180
|
+
|
181
|
+
activeValue: function () {
|
182
|
+
if (this.options.multiple) {
|
183
|
+
return this.el.value.split(/,\s*/).last();
|
184
|
+
} else {
|
185
|
+
return this.el.value;
|
186
|
+
}
|
187
|
+
},
|
188
|
+
|
189
|
+
getSuggestions: function() {
|
190
|
+
var newValue = this.activeValue();
|
191
|
+
console.log('getting suggestions for ', newValue);
|
192
|
+
var cr = this.cachedResponse[newValue];
|
193
|
+
if (cr && Object.isArray(cr.suggestions)) {
|
194
|
+
this.suggestions = cr.suggestions;
|
195
|
+
this.data = cr.data;
|
196
|
+
this.suggest();
|
197
|
+
} else if (!this.isBadQuery(newValue)) {
|
198
|
+
new Ajax.Request(this.serviceUrl, {
|
199
|
+
parameters: { query: newValue },
|
200
|
+
onComplete: this.processResponse.bind(this),
|
201
|
+
method: 'get'
|
202
|
+
});
|
203
|
+
}
|
204
|
+
},
|
205
|
+
|
206
|
+
isBadQuery: function(q) {
|
207
|
+
var i = this.badQueries.length;
|
208
|
+
while (i--) {
|
209
|
+
if (q.indexOf(this.badQueries[i]) === 0) { return true; }
|
210
|
+
}
|
211
|
+
return false;
|
212
|
+
},
|
213
|
+
|
214
|
+
hide: function() {
|
215
|
+
this.enabled = false;
|
216
|
+
this.selectedIndex = -1;
|
217
|
+
this.container.hide();
|
218
|
+
},
|
219
|
+
|
220
|
+
suggest: function() {
|
221
|
+
if (this.suggestions.length === 0) {
|
222
|
+
this.hide();
|
223
|
+
return;
|
224
|
+
}
|
225
|
+
var content = [];
|
226
|
+
var re = new RegExp('\\b' + this.activeValue().match(/\w+/g).join('|\\b'), 'gi');
|
227
|
+
this.suggestions.each(function(value, i) {
|
228
|
+
content.push((this.selectedIndex === i ? '<div class="selected"' : '<div'), ' title="', value, '" onclick="Autocomplete.instances[', this.instanceId, '].select(', i, ');" onmouseover="Autocomplete.instances[', this.instanceId, '].activate(', i, ');">', Autocomplete.highlight(value, re), '</div>');
|
229
|
+
} .bind(this));
|
230
|
+
this.enabled = true;
|
231
|
+
this.container.update(content.join('')).show();
|
232
|
+
},
|
233
|
+
|
234
|
+
processResponse: function(xhr) {
|
235
|
+
var response;
|
236
|
+
try {
|
237
|
+
response = xhr.responseText.evalJSON();
|
238
|
+
if (!Object.isArray(response.data)) { response.data = []; }
|
239
|
+
} catch (err) { return; }
|
240
|
+
this.cachedResponse[response.query] = response;
|
241
|
+
if (response.suggestions.length === 0) {
|
242
|
+
this.badQueries.push(response.query);
|
243
|
+
}
|
244
|
+
if (response.query === this.activeValue()) {
|
245
|
+
this.suggestions = response.suggestions;
|
246
|
+
this.data = response.data;
|
247
|
+
this.suggest();
|
248
|
+
}
|
249
|
+
},
|
250
|
+
|
251
|
+
activate: function(index) {
|
252
|
+
var divs = this.container.childNodes;
|
253
|
+
var activeItem;
|
254
|
+
// Clear previous selection:
|
255
|
+
if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) {
|
256
|
+
divs[this.selectedIndex].className = '';
|
257
|
+
}
|
258
|
+
this.selectedIndex = index;
|
259
|
+
if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) {
|
260
|
+
activeItem = divs[this.selectedIndex];
|
261
|
+
activeItem.className = 'selected';
|
262
|
+
}
|
263
|
+
return activeItem;
|
264
|
+
},
|
265
|
+
|
266
|
+
deactivate: function(div, index) {
|
267
|
+
div.className = '';
|
268
|
+
if (this.selectedIndex === index) { this.selectedIndex = -1; }
|
269
|
+
},
|
270
|
+
|
271
|
+
select: function(i) {
|
272
|
+
var selectedValue = this.suggestions[i];
|
273
|
+
if (selectedValue) {
|
274
|
+
this.updateValue(selectedValue);
|
275
|
+
if (this.options.autoSubmit && this.el.form) {
|
276
|
+
this.el.form.submit();
|
277
|
+
}
|
278
|
+
this.ignoreValueChange = true;
|
279
|
+
this.hide();
|
280
|
+
this.onSelect(i);
|
281
|
+
}
|
282
|
+
},
|
283
|
+
|
284
|
+
updateValue: function (selectedValue) {
|
285
|
+
if (this.options.multiple) {
|
286
|
+
var values = this.el.value.split(/,\s*/);
|
287
|
+
values.pop();
|
288
|
+
values.push(selectedValue, '');
|
289
|
+
this.el.value = values.uniq().join(', ');
|
290
|
+
|
291
|
+
} else {
|
292
|
+
this.el.value = selectedValue;
|
293
|
+
}
|
294
|
+
this.currentValue = this.el.value;
|
295
|
+
this.el.focus();
|
296
|
+
},
|
297
|
+
|
298
|
+
moveUp: function() {
|
299
|
+
if (this.selectedIndex === -1) { return; }
|
300
|
+
if (this.selectedIndex === 0) {
|
301
|
+
this.container.childNodes[0].className = '';
|
302
|
+
this.selectedIndex = -1;
|
303
|
+
this.updateValue(this.currentValue);
|
304
|
+
return;
|
305
|
+
}
|
306
|
+
this.adjustScroll(this.selectedIndex - 1);
|
307
|
+
},
|
308
|
+
|
309
|
+
moveDown: function() {
|
310
|
+
if (this.selectedIndex === (this.suggestions.length - 1)) { return; }
|
311
|
+
this.adjustScroll(this.selectedIndex + 1);
|
312
|
+
},
|
313
|
+
|
314
|
+
adjustScroll: function(i) {
|
315
|
+
var container = this.container;
|
316
|
+
var activeItem = this.activate(i);
|
317
|
+
var offsetTop = activeItem.offsetTop;
|
318
|
+
var upperBound = container.scrollTop;
|
319
|
+
var lowerBound = upperBound + this.options.maxHeight - 25;
|
320
|
+
if (offsetTop < upperBound) {
|
321
|
+
container.scrollTop = offsetTop;
|
322
|
+
} else if (offsetTop > lowerBound) {
|
323
|
+
container.scrollTop = offsetTop - this.options.maxHeight + 25;
|
324
|
+
}
|
325
|
+
this.updateValue(this.suggestions[i]);
|
326
|
+
},
|
327
|
+
|
328
|
+
onSelect: function(i) {
|
329
|
+
(this.options.onSelect || Prototype.emptyFunction)(this.suggestions[i], this.data[i]);
|
330
|
+
}
|
331
|
+
|
332
|
+
};
|
333
|
+
|
334
|
+
Event.observe(document, 'dom:loaded', function(){ Autocomplete.isDomLoaded = true; }, false);
|
@@ -0,0 +1,89 @@
|
|
1
|
+
@import compass/css3
|
2
|
+
|
3
|
+
// index
|
4
|
+
|
5
|
+
#tags-table
|
6
|
+
table
|
7
|
+
margin-bottom: 0
|
8
|
+
|
9
|
+
tr.secret
|
10
|
+
td
|
11
|
+
color: #999999
|
12
|
+
a
|
13
|
+
color: #999999
|
14
|
+
|
15
|
+
td.tag-title
|
16
|
+
font-size: 115%
|
17
|
+
font-weight: bold
|
18
|
+
|
19
|
+
td.tag-title a
|
20
|
+
text-decoration: none
|
21
|
+
color: black
|
22
|
+
|
23
|
+
td.tag-description
|
24
|
+
font-size: 80%
|
25
|
+
color: #999999
|
26
|
+
|
27
|
+
ul.cloud
|
28
|
+
list-style: none
|
29
|
+
margin: 0
|
30
|
+
padding: 0
|
31
|
+
line-height: 20px
|
32
|
+
ul.cloud li
|
33
|
+
display: inline
|
34
|
+
font-family: "Lucida Grande", "Bitstream Vera Sans", Helvetica, Verdana, Arial, sans-serif
|
35
|
+
letter-spacing: -0.04em
|
36
|
+
white-space: nowrap
|
37
|
+
line-height: 20px
|
38
|
+
margin: 0
|
39
|
+
|
40
|
+
// edit-page
|
41
|
+
|
42
|
+
p.title
|
43
|
+
width: 70%
|
44
|
+
|
45
|
+
p.keywords
|
46
|
+
width: 28%
|
47
|
+
float: right
|
48
|
+
margin: 0 1% 0 0
|
49
|
+
|
50
|
+
#content
|
51
|
+
form
|
52
|
+
.keywords
|
53
|
+
.textbox
|
54
|
+
font-family: Georgia, Palatino, "Times New Roman", Times, serif
|
55
|
+
font-size: 200%
|
56
|
+
width: 100%
|
57
|
+
color: #cc0000
|
58
|
+
margin-top: 4px
|
59
|
+
|
60
|
+
#attributes
|
61
|
+
clear: both
|
62
|
+
|
63
|
+
.autocomplete-w1
|
64
|
+
+box-shadow
|
65
|
+
position: absolute
|
66
|
+
top: 0
|
67
|
+
left: 0
|
68
|
+
background-color: #333
|
69
|
+
|
70
|
+
.autocomplete-w2
|
71
|
+
padding: 0
|
72
|
+
|
73
|
+
.autocomplete
|
74
|
+
font-family: Georgia, Palatino, "Times New Roman", Times, serif
|
75
|
+
width: 300px
|
76
|
+
opacity: 0.8
|
77
|
+
cursor: default
|
78
|
+
text-align: left
|
79
|
+
max-height: 350px
|
80
|
+
overflow: auto
|
81
|
+
margin: 0
|
82
|
+
div
|
83
|
+
padding: 5px 10px
|
84
|
+
white-space: nowrap
|
85
|
+
.selected
|
86
|
+
background: #f0f0f0
|
87
|
+
strong
|
88
|
+
font-weight: normal
|
89
|
+
color: #3399ff
|
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{radiant-taggable-extension}
|
8
|
-
s.version = "1.2.
|
8
|
+
s.version = "1.2.4"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["spanner"]
|
12
|
-
s.date = %q{2011-
|
12
|
+
s.date = %q{2011-04-12}
|
13
13
|
s.description = %q{General purpose tagging extension: more versatile but less focused than the tags extension}
|
14
14
|
s.email = %q{will@spanner.org}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -22,6 +22,7 @@ Gem::Specification.new do |s|
|
|
22
22
|
"VERSION",
|
23
23
|
"app/controllers/admin/taggings_controller.rb",
|
24
24
|
"app/controllers/admin/tags_controller.rb",
|
25
|
+
"app/helpers/taggable_helper.rb",
|
25
26
|
"app/models/tag.rb",
|
26
27
|
"app/models/tagging.rb",
|
27
28
|
"app/views/admin/pages/_edit_title.html.haml",
|
@@ -32,9 +33,12 @@ Gem::Specification.new do |s|
|
|
32
33
|
"app/views/admin/tags/index.html.haml",
|
33
34
|
"app/views/admin/tags/new.html.haml",
|
34
35
|
"app/views/admin/tags/show.html.haml",
|
36
|
+
"config/locales/en.yml",
|
35
37
|
"config/routes.rb",
|
36
38
|
"db/migrate/001_create_tags.rb",
|
37
39
|
"db/migrate/002_import_keywords.rb",
|
40
|
+
"db/migrate/20110316210834_structural_tags.rb",
|
41
|
+
"db/migrate/20110411075109_metaphones.rb",
|
38
42
|
"lib/natcmp.rb",
|
39
43
|
"lib/radiant-taggable-extension.rb",
|
40
44
|
"lib/taggable_admin_page_controller.rb",
|
@@ -43,9 +47,13 @@ Gem::Specification.new do |s|
|
|
43
47
|
"lib/taggable_page.rb",
|
44
48
|
"lib/taggable_tags.rb",
|
45
49
|
"lib/tasks/taggable_extension_tasks.rake",
|
50
|
+
"lib/text/double_metaphone.rb",
|
51
|
+
"lib/text/metaphone.rb",
|
46
52
|
"public/images/admin/new-tag.png",
|
47
53
|
"public/images/admin/tag.png",
|
48
|
-
"public/
|
54
|
+
"public/javascripts/admin/taggable.js",
|
55
|
+
"public/javascripts/autocomplete.js",
|
56
|
+
"public/stylesheets/sass/admin/taggable.sass",
|
49
57
|
"public/stylesheets/sass/tagcloud.sass",
|
50
58
|
"radiant-taggable-extension.gemspec",
|
51
59
|
"spec/datasets/tag_sites_dataset.rb",
|