redmine_crm 0.0.16 → 0.0.17

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8476cc7f68dab5781c42ea785cdf2ceee5cd110f
4
- data.tar.gz: 803b10a20986f745450fb3137b69c69d6fa53158
3
+ metadata.gz: 8a808b98bd2a294b9250efe647f0b4eb834b0d0c
4
+ data.tar.gz: a54a30d61a77caa97b6e5477dbed6ac51f329881
5
5
  SHA512:
6
- metadata.gz: 8a3fbac887b278b4f45ca4dc48adc508ed5876e9a5ff8bb531a6b837a8a8d9b4b0ff8d6a05df5165701030c2ec3e9c50a5f622660f25a474be2eba13b6e0e1b8
7
- data.tar.gz: c14842d725949500bd2d91121f6817864908b138c899883f0b28bd83318f7c700ac18cf47bc9c8af3e4705c43084eaa845bbac697016a1e2c48906f3a3fb07db
6
+ metadata.gz: 6221ee71b59ec5edd31fdd02347e34e4a4e9d77499aa30d7710342fc55c39ba1b0d03f01eabb9a3f2e9f1554cc68a7efcf22c5098704b2bd01e5ba327599363d
7
+ data.tar.gz: 72e83c84fc1b0f15ad0363ac6f8be46d7b5ed343ca1ffa62039dc401cdbeeea299147931328f27753ac7c021d83f10b99126362181f2777215ad27e7dca8b17c
data/README.md CHANGED
@@ -44,8 +44,16 @@ Add to model
44
44
 
45
45
  ```ruby
46
46
  rcrm_acts_as_taggable
47
+ rcrm_acts_as_votable
47
48
  ```
48
49
 
50
+ For use css and js from gem you may use code like this:
51
+
52
+ <% content_for :header_tags do %>
53
+ <%= javascript_include_tag "redmine_crm" %>
54
+ <%= stylesheet_link_tag "redmine_crm" %>
55
+ <% end %>
56
+
49
57
  ## Run test
50
58
 
51
59
  ```
@@ -0,0 +1 @@
1
+ //= require_tree .
@@ -0,0 +1,590 @@
1
+ /*
2
+ * jQuery UI Tag-it!
3
+ *
4
+ * @version v2.0 (06/2011)
5
+ *
6
+ * Copyright 2011, Levy Carneiro Jr.
7
+ * Released under the MIT license.
8
+ * http://aehlke.github.com/tag-it/LICENSE
9
+ *
10
+ * Homepage:
11
+ * http://aehlke.github.com/tag-it/
12
+ *
13
+ * Authors:
14
+ * Levy Carneiro Jr.
15
+ * Martin Rehfeld
16
+ * Tobias Schmidt
17
+ * Skylar Challand
18
+ * Alex Ehlke
19
+ *
20
+ * Maintainer:
21
+ * Alex Ehlke - Twitter: @aehlke
22
+ *
23
+ * Dependencies:
24
+ * jQuery v1.4+
25
+ * jQuery UI v1.8+
26
+ */
27
+ (function($) {
28
+
29
+ $.widget('ui.tagit', {
30
+ options: {
31
+ allowDuplicates : false,
32
+ caseSensitive : true,
33
+ fieldName : 'tags',
34
+ placeholderText : null, // Sets `placeholder` attr on input field.
35
+ readOnly : false, // Disables editing.
36
+ removeConfirmation: false, // Require confirmation to remove tags.
37
+ tagLimit : null, // Max number of tags allowed (null for unlimited).
38
+
39
+ // Used for autocomplete, unless you override `autocomplete.source`.
40
+ availableTags : [],
41
+
42
+ // Use to override or add any options to the autocomplete widget.
43
+ //
44
+ // By default, autocomplete.source will map to availableTags,
45
+ // unless overridden.
46
+ autocomplete: {},
47
+
48
+ // Shows autocomplete before the user even types anything.
49
+ showAutocompleteOnFocus: false,
50
+
51
+ // When enabled, quotes are unneccesary for inputting multi-word tags.
52
+ allowSpaces: false,
53
+
54
+ // The below options are for using a single field instead of several
55
+ // for our form values.
56
+ //
57
+ // When enabled, will use a single hidden field for the form,
58
+ // rather than one per tag. It will delimit tags in the field
59
+ // with singleFieldDelimiter.
60
+ //
61
+ // The easiest way to use singleField is to just instantiate tag-it
62
+ // on an INPUT element, in which case singleField is automatically
63
+ // set to true, and singleFieldNode is set to that element. This
64
+ // way, you don't need to fiddle with these options.
65
+ singleField: false,
66
+
67
+ // This is just used when preloading data from the field, and for
68
+ // populating the field with delimited tags as the user adds them.
69
+ singleFieldDelimiter: ',',
70
+
71
+ // Set this to an input DOM node to use an existing form field.
72
+ // Any text in it will be erased on init. But it will be
73
+ // populated with the text of tags as they are created,
74
+ // delimited by singleFieldDelimiter.
75
+ //
76
+ // If this is not set, we create an input node for it,
77
+ // with the name given in settings.fieldName.
78
+ singleFieldNode: null,
79
+
80
+ // Whether to animate tag removals or not.
81
+ animate: true,
82
+
83
+ // Optionally set a tabindex attribute on the input that gets
84
+ // created for tag-it.
85
+ tabIndex: null,
86
+
87
+ // Event callbacks.
88
+ beforeTagAdded : null,
89
+ afterTagAdded : null,
90
+
91
+ beforeTagRemoved : null,
92
+ afterTagRemoved : null,
93
+
94
+ onTagClicked : null,
95
+ onTagLimitExceeded : null,
96
+
97
+
98
+ // DEPRECATED:
99
+ //
100
+ // /!\ These event callbacks are deprecated and WILL BE REMOVED at some
101
+ // point in the future. They're here for backwards-compatibility.
102
+ // Use the above before/after event callbacks instead.
103
+ onTagAdded : null,
104
+ onTagRemoved: null,
105
+ // `autocomplete.source` is the replacement for tagSource.
106
+ tagSource: null
107
+ // Do not use the above deprecated options.
108
+ },
109
+
110
+ _create: function() {
111
+ // for handling static scoping inside callbacks
112
+ var that = this;
113
+
114
+ // There are 2 kinds of DOM nodes this widget can be instantiated on:
115
+ // 1. UL, OL, or some element containing either of these.
116
+ // 2. INPUT, in which case 'singleField' is overridden to true,
117
+ // a UL is created and the INPUT is hidden.
118
+ if (this.element.is('input')) {
119
+ this.tagList = $('<ul></ul>').insertAfter(this.element);
120
+ this.options.singleField = true;
121
+ this.options.singleFieldNode = this.element;
122
+ this.element.addClass('tagit-hidden-field');
123
+ } else {
124
+ this.tagList = this.element.find('ul, ol').andSelf().last();
125
+ }
126
+
127
+ this.tagInput = $('<input type="text" />').addClass('ui-widget-content');
128
+
129
+ if (this.options.readOnly) this.tagInput.attr('disabled', 'disabled');
130
+
131
+ if (this.options.tabIndex) {
132
+ this.tagInput.attr('tabindex', this.options.tabIndex);
133
+ }
134
+
135
+ if (this.options.placeholderText) {
136
+ this.tagInput.attr('placeholder', this.options.placeholderText);
137
+ }
138
+
139
+ if (!this.options.autocomplete.source) {
140
+ this.options.autocomplete.source = function(search, showChoices) {
141
+ var filter = search.term.toLowerCase();
142
+ var choices = $.grep(this.options.availableTags, function(element) {
143
+ // Only match autocomplete options that begin with the search term.
144
+ // (Case insensitive.)
145
+ return (element.toLowerCase().indexOf(filter) === 0);
146
+ });
147
+ if (!this.options.allowDuplicates) {
148
+ choices = this._subtractArray(choices, this.assignedTags());
149
+ }
150
+ showChoices(choices);
151
+ };
152
+ }
153
+
154
+ if (this.options.showAutocompleteOnFocus) {
155
+ this.tagInput.focus(function(event, ui) {
156
+ that._showAutocomplete();
157
+ });
158
+
159
+ if (typeof this.options.autocomplete.minLength === 'undefined') {
160
+ this.options.autocomplete.minLength = 0;
161
+ }
162
+ }
163
+
164
+ // Bind autocomplete.source callback functions to this context.
165
+ if ($.isFunction(this.options.autocomplete.source)) {
166
+ this.options.autocomplete.source = $.proxy(this.options.autocomplete.source, this);
167
+ }
168
+
169
+ // DEPRECATED.
170
+ if ($.isFunction(this.options.tagSource)) {
171
+ this.options.tagSource = $.proxy(this.options.tagSource, this);
172
+ }
173
+
174
+ this.tagList
175
+ .addClass('tagit')
176
+ .addClass('ui-widget ui-widget-content ui-corner-all')
177
+ // Create the input field.
178
+ .append($('<li class="tagit-new"></li>').append(this.tagInput))
179
+ .click(function(e) {
180
+ var target = $(e.target);
181
+ if (target.hasClass('tagit-label')) {
182
+ var tag = target.closest('.tagit-choice');
183
+ if (!tag.hasClass('removed')) {
184
+ that._trigger('onTagClicked', e, {tag: tag, tagLabel: that.tagLabel(tag)});
185
+ }
186
+ } else {
187
+ // Sets the focus() to the input field, if the user
188
+ // clicks anywhere inside the UL. This is needed
189
+ // because the input field needs to be of a small size.
190
+ that.tagInput.focus();
191
+ }
192
+ });
193
+
194
+ // Single field support.
195
+ var addedExistingFromSingleFieldNode = false;
196
+ if (this.options.singleField) {
197
+ if (this.options.singleFieldNode) {
198
+ // Add existing tags from the input field.
199
+ var node = $(this.options.singleFieldNode);
200
+ var tags = node.val().split(this.options.singleFieldDelimiter);
201
+ node.val('');
202
+ $.each(tags, function(index, tag) {
203
+ that.createTag(tag, null, true);
204
+ addedExistingFromSingleFieldNode = true;
205
+ });
206
+ } else {
207
+ // Create our single field input after our list.
208
+ this.options.singleFieldNode = $('<input type="hidden" style="display:none;" value="" name="' + this.options.fieldName + '" />');
209
+ this.tagList.after(this.options.singleFieldNode);
210
+ }
211
+ }
212
+
213
+ // Add existing tags from the list, if any.
214
+ if (!addedExistingFromSingleFieldNode) {
215
+ this.tagList.children('li').each(function() {
216
+ if (!$(this).hasClass('tagit-new')) {
217
+ that.createTag($(this).text(), $(this).attr('class'), true);
218
+ $(this).remove();
219
+ }
220
+ });
221
+ }
222
+
223
+ // Events.
224
+ this.tagInput
225
+ .keydown(function(event) {
226
+ // Backspace is not detected within a keypress, so it must use keydown.
227
+ if (event.which == $.ui.keyCode.BACKSPACE && that.tagInput.val() === '') {
228
+ var tag = that._lastTag();
229
+ if (!that.options.removeConfirmation || tag.hasClass('remove')) {
230
+ // When backspace is pressed, the last tag is deleted.
231
+ that.removeTag(tag);
232
+ } else if (that.options.removeConfirmation) {
233
+ tag.addClass('remove ui-state-highlight');
234
+ }
235
+ } else if (that.options.removeConfirmation) {
236
+ that._lastTag().removeClass('remove ui-state-highlight');
237
+ }
238
+
239
+ // Comma/Space/Enter are all valid delimiters for new tags,
240
+ // except when there is an open quote or if setting allowSpaces = true.
241
+ // Tab will also create a tag, unless the tag input is empty,
242
+ // in which case it isn't caught.
243
+ if (
244
+ (event.which === $.ui.keyCode.COMMA && event.shiftKey === false) ||
245
+ event.which === $.ui.keyCode.ENTER ||
246
+ (
247
+ event.which == $.ui.keyCode.TAB &&
248
+ that.tagInput.val() !== ''
249
+ ) ||
250
+ (
251
+ event.which == $.ui.keyCode.SPACE &&
252
+ that.options.allowSpaces !== true &&
253
+ (
254
+ $.trim(that.tagInput.val()).replace( /^s*/, '' ).charAt(0) != '"' ||
255
+ (
256
+ $.trim(that.tagInput.val()).charAt(0) == '"' &&
257
+ $.trim(that.tagInput.val()).charAt($.trim(that.tagInput.val()).length - 1) == '"' &&
258
+ $.trim(that.tagInput.val()).length - 1 !== 0
259
+ )
260
+ )
261
+ )
262
+ ) {
263
+ // Enter submits the form if there's no text in the input.
264
+ if (!(event.which === $.ui.keyCode.ENTER && that.tagInput.val() === '')) {
265
+ event.preventDefault();
266
+ }
267
+
268
+ // Autocomplete will create its own tag from a selection and close automatically.
269
+ if (!(that.options.autocomplete.autoFocus && that.tagInput.data('autocomplete-open'))) {
270
+ that.tagInput.autocomplete('close');
271
+ that.createTag(that._cleanedInput());
272
+ }
273
+ }
274
+ }).blur(function(e){
275
+ // Create a tag when the element loses focus.
276
+ // If autocomplete is enabled and suggestion was clicked, don't add it.
277
+ if (!that.tagInput.data('autocomplete-open')) {
278
+ that.createTag(that._cleanedInput());
279
+ }
280
+ });
281
+
282
+ // Autocomplete.
283
+ if (this.options.availableTags || this.options.tagSource || this.options.autocomplete.source) {
284
+ var autocompleteOptions = {
285
+ select: function(event, ui) {
286
+ that.createTag(ui.item.value);
287
+ // Preventing the tag input to be updated with the chosen value.
288
+ return false;
289
+ }
290
+ };
291
+ $.extend(autocompleteOptions, this.options.autocomplete);
292
+
293
+ // tagSource is deprecated, but takes precedence here since autocomplete.source is set by default,
294
+ // while tagSource is left null by default.
295
+ autocompleteOptions.source = this.options.tagSource || autocompleteOptions.source;
296
+
297
+ this.tagInput.autocomplete(autocompleteOptions).bind('autocompleteopen.tagit', function(event, ui) {
298
+ that.tagInput.data('autocomplete-open', true);
299
+ }).bind('autocompleteclose.tagit', function(event, ui) {
300
+ that.tagInput.data('autocomplete-open', false);
301
+ });
302
+
303
+ this.tagInput.autocomplete('widget').addClass('tagit-autocomplete');
304
+ }
305
+ },
306
+
307
+ destroy: function() {
308
+ $.Widget.prototype.destroy.call(this);
309
+
310
+ this.element.unbind('.tagit');
311
+ this.tagList.unbind('.tagit');
312
+
313
+ this.tagInput.removeData('autocomplete-open');
314
+
315
+ this.tagList.removeClass([
316
+ 'tagit',
317
+ 'ui-widget',
318
+ 'ui-widget-content',
319
+ 'ui-corner-all',
320
+ 'tagit-hidden-field'
321
+ ].join(' '));
322
+
323
+ if (this.element.is('input')) {
324
+ this.element.removeClass('tagit-hidden-field');
325
+ this.tagList.remove();
326
+ } else {
327
+ this.element.children('li').each(function() {
328
+ if ($(this).hasClass('tagit-new')) {
329
+ $(this).remove();
330
+ } else {
331
+ $(this).removeClass([
332
+ 'tagit-choice',
333
+ 'ui-widget-content',
334
+ 'ui-state-default',
335
+ 'ui-state-highlight',
336
+ 'ui-corner-all',
337
+ 'remove',
338
+ 'tagit-choice-editable',
339
+ 'tagit-choice-read-only'
340
+ ].join(' '));
341
+
342
+ $(this).text($(this).children('.tagit-label').text());
343
+ }
344
+ });
345
+
346
+ if (this.singleFieldNode) {
347
+ this.singleFieldNode.remove();
348
+ }
349
+ }
350
+
351
+ return this;
352
+ },
353
+
354
+ _cleanedInput: function() {
355
+ // Returns the contents of the tag input, cleaned and ready to be passed to createTag
356
+ return $.trim(this.tagInput.val().replace(/^"(.*)"$/, '$1'));
357
+ },
358
+
359
+ _lastTag: function() {
360
+ return this.tagList.find('.tagit-choice:last:not(.removed)');
361
+ },
362
+
363
+ _tags: function() {
364
+ return this.tagList.find('.tagit-choice:not(.removed)');
365
+ },
366
+
367
+ assignedTags: function() {
368
+ // Returns an array of tag string values
369
+ var that = this;
370
+ var tags = [];
371
+ if (this.options.singleField) {
372
+ tags = $(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter);
373
+ if (tags[0] === '') {
374
+ tags = [];
375
+ }
376
+ } else {
377
+ this._tags().each(function() {
378
+ tags.push(that.tagLabel(this));
379
+ });
380
+ }
381
+ return tags;
382
+ },
383
+
384
+ _updateSingleTagsField: function(tags) {
385
+ // Takes a list of tag string values, updates this.options.singleFieldNode.val to the tags delimited by this.options.singleFieldDelimiter
386
+ $(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter)).trigger('change');
387
+ },
388
+
389
+ _subtractArray: function(a1, a2) {
390
+ var result = [];
391
+ for (var i = 0; i < a1.length; i++) {
392
+ if ($.inArray(a1[i], a2) == -1) {
393
+ result.push(a1[i]);
394
+ }
395
+ }
396
+ return result;
397
+ },
398
+
399
+ tagLabel: function(tag) {
400
+ // Returns the tag's string label.
401
+ if (this.options.singleField) {
402
+ return $(tag).find('.tagit-label:first').text();
403
+ } else {
404
+ return $(tag).find('input:first').val();
405
+ }
406
+ },
407
+
408
+ _showAutocomplete: function() {
409
+ this.tagInput.autocomplete('search', '');
410
+ },
411
+
412
+ _findTagByLabel: function(name) {
413
+ var that = this;
414
+ var tag = null;
415
+ this._tags().each(function(i) {
416
+ if (that._formatStr(name) == that._formatStr(that.tagLabel(this))) {
417
+ tag = $(this);
418
+ return false;
419
+ }
420
+ });
421
+ return tag;
422
+ },
423
+
424
+ _isNew: function(name) {
425
+ return !this._findTagByLabel(name);
426
+ },
427
+
428
+ _formatStr: function(str) {
429
+ if (this.options.caseSensitive) {
430
+ return str;
431
+ }
432
+ return $.trim(str.toLowerCase());
433
+ },
434
+
435
+ _effectExists: function(name) {
436
+ return Boolean($.effects && ($.effects[name] || ($.effects.effect && $.effects.effect[name])));
437
+ },
438
+
439
+ createTag: function(value, additionalClass, duringInitialization) {
440
+ var that = this;
441
+
442
+ value = $.trim(value);
443
+
444
+ if(this.options.preprocessTag) {
445
+ value = this.options.preprocessTag(value);
446
+ }
447
+
448
+ if (value === '') {
449
+ return false;
450
+ }
451
+
452
+ if (!this.options.allowDuplicates && !this._isNew(value)) {
453
+ var existingTag = this._findTagByLabel(value);
454
+ if (this._trigger('onTagExists', null, {
455
+ existingTag: existingTag,
456
+ duringInitialization: duringInitialization
457
+ }) !== false) {
458
+ if (this._effectExists('highlight')) {
459
+ existingTag.effect('highlight');
460
+ }
461
+ }
462
+ return false;
463
+ }
464
+
465
+ if (this.options.tagLimit && this._tags().length >= this.options.tagLimit) {
466
+ this._trigger('onTagLimitExceeded', null, {duringInitialization: duringInitialization});
467
+ return false;
468
+ }
469
+
470
+ var label = $(this.options.onTagClicked ? '<a class="tagit-label"></a>' : '<span class="tagit-label"></span>').text(value);
471
+
472
+ // Create tag.
473
+ var tag = $('<li></li>')
474
+ .addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all')
475
+ .addClass(additionalClass)
476
+ .append(label);
477
+
478
+ if (this.options.readOnly){
479
+ tag.addClass('tagit-choice-read-only');
480
+ } else {
481
+ tag.addClass('tagit-choice-editable');
482
+ // Button for removing the tag.
483
+ var removeTagIcon = $('<span></span>')
484
+ .addClass('ui-icon ui-icon-close');
485
+ var removeTag = $('<a><span class="text-icon">\xd7</span></a>') // \xd7 is an X
486
+ .addClass('tagit-close')
487
+ .append(removeTagIcon)
488
+ .click(function(e) {
489
+ // Removes a tag when the little 'x' is clicked.
490
+ that.removeTag(tag);
491
+ });
492
+ tag.append(removeTag);
493
+ }
494
+
495
+ // Unless options.singleField is set, each tag has a hidden input field inline.
496
+ if (!this.options.singleField) {
497
+ var escapedValue = label.html();
498
+ tag.append('<input type="hidden" value="' + escapedValue + '" name="' + this.options.fieldName + '" class="tagit-hidden-field" />');
499
+ }
500
+
501
+ if (this._trigger('beforeTagAdded', null, {
502
+ tag: tag,
503
+ tagLabel: this.tagLabel(tag),
504
+ duringInitialization: duringInitialization
505
+ }) === false) {
506
+ return;
507
+ }
508
+
509
+ if (this.options.singleField) {
510
+ var tags = this.assignedTags();
511
+ tags.push(value);
512
+ this._updateSingleTagsField(tags);
513
+ }
514
+
515
+ // DEPRECATED.
516
+ this._trigger('onTagAdded', null, tag);
517
+
518
+ this.tagInput.val('');
519
+
520
+ // Insert tag.
521
+ this.tagInput.parent().before(tag);
522
+
523
+ this._trigger('afterTagAdded', null, {
524
+ tag: tag,
525
+ tagLabel: this.tagLabel(tag),
526
+ duringInitialization: duringInitialization
527
+ });
528
+
529
+ if (this.options.showAutocompleteOnFocus && !duringInitialization) {
530
+ setTimeout(function () { that._showAutocomplete(); }, 0);
531
+ }
532
+ },
533
+
534
+ removeTag: function(tag, animate) {
535
+ animate = typeof animate === 'undefined' ? this.options.animate : animate;
536
+
537
+ tag = $(tag);
538
+
539
+ // DEPRECATED.
540
+ this._trigger('onTagRemoved', null, tag);
541
+
542
+ if (this._trigger('beforeTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)}) === false) {
543
+ return;
544
+ }
545
+
546
+ if (this.options.singleField) {
547
+ var tags = this.assignedTags();
548
+ var removedTagLabel = this.tagLabel(tag);
549
+ tags = $.grep(tags, function(el){
550
+ return el != removedTagLabel;
551
+ });
552
+ this._updateSingleTagsField(tags);
553
+ }
554
+
555
+ if (animate) {
556
+ tag.addClass('removed'); // Excludes this tag from _tags.
557
+ var hide_args = this._effectExists('blind') ? ['blind', {direction: 'horizontal'}, 'fast'] : ['fast'];
558
+
559
+ var thisTag = this;
560
+ hide_args.push(function() {
561
+ tag.remove();
562
+ thisTag._trigger('afterTagRemoved', null, {tag: tag, tagLabel: thisTag.tagLabel(tag)});
563
+ });
564
+
565
+ tag.fadeOut('fast').hide.apply(tag, hide_args).dequeue();
566
+ } else {
567
+ tag.remove();
568
+ this._trigger('afterTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)});
569
+ }
570
+
571
+ },
572
+
573
+ removeTagByLabel: function(tagLabel, animate) {
574
+ var toRemove = this._findTagByLabel(tagLabel);
575
+ if (!toRemove) {
576
+ throw "No such tag exists with the name '" + tagLabel + "'";
577
+ }
578
+ this.removeTag(toRemove, animate);
579
+ },
580
+
581
+ removeAll: function() {
582
+ // Removes all tags.
583
+ var that = this;
584
+ this._tags().each(function(index, tag) {
585
+ that.removeTag(tag, false);
586
+ });
587
+ }
588
+
589
+ });
590
+ })(jQuery);
@@ -0,0 +1,54 @@
1
+ ul.tagit {
2
+ padding: 1px 5px;
3
+ overflow: auto;
4
+ margin-left: inherit; /* usually we don't want the regular ul margins. */
5
+ margin-right: inherit;
6
+ }
7
+ ul.tagit li {
8
+ display: block;
9
+ float: left;
10
+ margin: 2px 5px 2px 0;
11
+ }
12
+ ul.tagit li.tagit-choice {
13
+ padding: .2em 18px .2em .5em;
14
+ position: relative;
15
+ line-height: inherit;
16
+ }
17
+ ul.tagit li.tagit-new {
18
+ padding: .25em 4px .25em 0;
19
+ }
20
+
21
+ ul.tagit li.tagit-choice a.tagit-label {
22
+ cursor: pointer;
23
+ text-decoration: none;
24
+ }
25
+ ul.tagit li.tagit-choice .tagit-close {
26
+ cursor: pointer;
27
+ position: absolute;
28
+ right: .1em;
29
+ top: 50%;
30
+ margin-top: -8px;
31
+ }
32
+
33
+ /* used for some custom themes that don't need image icons */
34
+ ul.tagit li.tagit-choice .tagit-close .text-icon {
35
+ display: none;
36
+ }
37
+
38
+ ul.tagit li.tagit-choice input {
39
+ display: block;
40
+ float: left;
41
+ margin: 2px 5px 2px 0;
42
+ }
43
+ ul.tagit input[type="text"] {
44
+ -moz-box-sizing: border-box;
45
+ -webkit-box-sizing: border-box;
46
+ box-sizing: border-box;
47
+
48
+ border: none;
49
+ margin: 0;
50
+ padding: 0;
51
+ width: inherit;
52
+ background-color: inherit;
53
+ outline: none;
54
+ }
@@ -0,0 +1,3 @@
1
+ /*
2
+ *= require_tree .
3
+ */
data/doc/CHANGELOG CHANGED
@@ -4,6 +4,11 @@ Redmine crm gem - general functions for plugins (tags, vote, viewing, currency)
4
4
  Copyright (C) 2011-2015 RedmineCRM
5
5
  http://www.redminecrm.com/
6
6
 
7
+ == 2015-10-09 v0.0.17
8
+
9
+ * Added js and css for tags
10
+ * Added function 'requires_version' for check version of gem from Redmine plugin
11
+
7
12
  == 2015-09-26 v0.0.16
8
13
 
9
14
  * Totally remove dependency from Contact plugin
data/lib/redmine_crm.rb CHANGED
@@ -18,4 +18,45 @@ require "redmine_crm/rcrm_acts_as_viewed"
18
18
  if defined?(ActiveRecord::Base)
19
19
  ActiveRecord::Base.extend(RedmineCrm::ActsAsVotable::Votable)
20
20
  ActiveRecord::Base.extend(RedmineCrm::ActsAsVotable::Voter)
21
+ end
22
+
23
+ module RedmineCrm
24
+
25
+ # class PluginNotFound < StandardError; end
26
+ class GemRequirementError < StandardError; end
27
+
28
+ def self.compare_versions(requirement, current)
29
+ requirement = requirement.split('.').collect(&:to_i)
30
+ requirement <=> current.slice(0, requirement.size)
31
+ end
32
+
33
+ def self.requires_version(plugin, arg)
34
+ arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
35
+ arg.assert_valid_keys(:version, :version_or_higher)
36
+
37
+ current = VERSION.split(".").map{|x| x.to_i}
38
+ arg.each do |k, req|
39
+ case k
40
+ when :version_or_higher
41
+ raise ArgumentError.new(":version_or_higher accepts a version string only") unless req.is_a?(String)
42
+ unless compare_versions(req, current) <= 0
43
+ raise GemRequirementError.new("#{plugin} plugin requires RedmineCrm gem #{req} or higher but current is #{current.join('.')}")
44
+ end
45
+ when :version
46
+ req = [req] if req.is_a?(String)
47
+ if req.is_a?(Array)
48
+ unless req.detect {|ver| compare_versions(ver, current) == 0}
49
+ raise GemRequirementError.new("#{plugin} plugin requires one the following RedmineCrm versions: #{req.join(', ')} but current is #{current.join('.')}")
50
+ end
51
+ elsif req.is_a?(Range)
52
+ unless compare_versions(req.first, current) <= 0 && compare_versions(req.last, current) >= 0
53
+ raise GemRequirementError.new("#{plugin} plugin requires a RedmineCrm version between #{req.first} and #{req.last} but current is #{current.join('.')}")
54
+ end
55
+ else
56
+ raise ArgumentError.new(":version option accepts a version string, an array or a range of versions")
57
+ end
58
+ end
59
+ end
60
+ true
61
+ end
21
62
  end
@@ -45,6 +45,7 @@ module RedmineCrm
45
45
  # Money::Currency.find(:eur) #=> #<Money::Currency id: eur ...>
46
46
  # Money::Currency.find(:foo) #=> nil
47
47
  def find(id)
48
+ return nil if id == ""
48
49
  id = id.to_s.downcase.to_sym
49
50
  new(id)
50
51
  rescue UnknownCurrency
@@ -168,9 +169,9 @@ module RedmineCrm
168
169
  # if it didn't.
169
170
  def unregister(curr)
170
171
  if curr.is_a?(Hash)
171
- key = curr.fetch(:iso_code).downcase.to_sym
172
+ key = curr.fetch(:iso_code).to_s.downcase.to_sym
172
173
  else
173
- key = curr.downcase.to_sym
174
+ key = curr.to_s.downcase.to_sym
174
175
  end
175
176
  existed = @table.delete(key)
176
177
  @stringified_keys = stringify_keys
@@ -267,10 +268,10 @@ module RedmineCrm
267
268
  # c1 <=> c1 #=> 0
268
269
  def <=>(other_currency)
269
270
  # <=> returns nil when one of the values is nil
270
- comparison = self.priority <=> other_currency.priority || 0
271
+ comparison = (self.priority <=> other_currency.priority || 0) rescue 0
271
272
 
272
273
  if comparison == 0
273
- self.id <=> other_currency.id
274
+ self.id.to_s <=> other_currency.id.to_s
274
275
  else
275
276
  comparison
276
277
  end
@@ -385,7 +386,7 @@ module RedmineCrm
385
386
  #
386
387
  # @return [Float]
387
388
  def exponent
388
- Math.log10(@subunit_to_unit)
389
+ Math.log10(@subunit_to_unit) if @subunit_to_unit
389
390
  end
390
391
 
391
392
  # Cache decimal places for subunit_to_unit values. Common ones pre-cached.
@@ -1,3 +1,3 @@
1
1
  module RedmineCrm
2
- VERSION = "0.0.16"
2
+ VERSION = "0.0.17"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redmine_crm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.16
4
+ version: 0.0.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - RedmineCRM
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-09-26 00:00:00.000000000 Z
11
+ date: 2015-10-09 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: plugins for Redmine
14
14
  email:
@@ -21,11 +21,13 @@ files:
21
21
  - Gemfile
22
22
  - README.md
23
23
  - Rakefile
24
+ - app/assets/javascripts/redmine_crm.js
25
+ - app/assets/javascripts/tag-it.js
26
+ - app/assets/stylesheets/jquery.tagit.css
27
+ - app/assets/stylesheets/redmine_crm.css
24
28
  - config/currency_iso.json
25
29
  - doc/CHANGELOG
26
30
  - doc/LICENSE.txt
27
- - lib/generators/redmine_crm_migration/redmine_crm_migration_generator.rb
28
- - lib/generators/redmine_crm_migration/templates/migration.rb
29
31
  - lib/redmine_crm.rb
30
32
  - lib/redmine_crm/currency.rb
31
33
  - lib/redmine_crm/currency/formatting.rb
@@ -1,13 +0,0 @@
1
- module RedmineCrm
2
- class RedmineCrmMigrationGenerator < Rails::Generator::Base
3
- def manifest
4
- record do |m|
5
- m.migration_template 'migration.rb', 'db/migrate'
6
- end
7
- end
8
-
9
- def file_name
10
- "redmine_crm_migration"
11
- end
12
- end
13
- end
@@ -1,26 +0,0 @@
1
- class RedminCrmMigration < ActiveRecord::Migration
2
- def self.up
3
- create_table :tags do |t|
4
- t.column :name, :string
5
- end
6
-
7
- create_table :taggings do |t|
8
- t.column :tag_id, :integer
9
- t.column :taggable_id, :integer
10
-
11
- # You should make sure that the column created is
12
- # long enough to store the required class names.
13
- t.column :taggable_type, :string
14
-
15
- t.column :created_at, :datetime
16
- end
17
-
18
- add_index :taggings, :tag_id
19
- add_index :taggings, [:taggable_id, :taggable_type]
20
- end
21
-
22
- def self.down
23
- drop_table :taggings
24
- drop_table :tags
25
- end
26
- end