social_stream 2.1.1 → 2.2.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.
Files changed (188) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +5 -2
  3. data/LICENSE +1 -1
  4. data/base/Rakefile +3 -17
  5. data/base/app/assets/images/flags/de.png +0 -0
  6. data/base/app/assets/images/flags/fr.png +0 -0
  7. data/base/app/assets/images/flags/hu.png +0 -0
  8. data/base/app/assets/images/flags/nl.png +0 -0
  9. data/base/app/assets/javascripts/social_stream/actor.js +34 -0
  10. data/base/app/assets/javascripts/social_stream/contact.js +40 -5
  11. data/base/app/assets/javascripts/social_stream/flash.js +6 -1
  12. data/base/app/assets/javascripts/social_stream/group.js +6 -5
  13. data/base/app/assets/javascripts/social_stream/relation_customs.js +0 -1
  14. data/base/app/assets/stylesheets/social_stream/base/adjust/layout/_adjust.css.sass +3 -0
  15. data/base/app/assets/stylesheets/social_stream/base/buttons/_buttons.scss.sass +0 -2
  16. data/base/app/assets/stylesheets/social_stream/base/contacts/_contacts.css.sass +1 -11
  17. data/base/app/assets/stylesheets/social_stream/base/contacts/layouts/_contacts.css.sass +1 -29
  18. data/base/app/assets/stylesheets/social_stream/base/layouts/_header.css.sass +0 -1
  19. data/base/app/assets/stylesheets/social_stream/base/mixins/_buttons.css.sass +5 -0
  20. data/base/app/assets/stylesheets/social_stream/base/mixins/_layout.css.sass +36 -7
  21. data/base/app/controllers/actors_controller.rb +25 -0
  22. data/base/app/controllers/contacts_controller.rb +38 -8
  23. data/base/app/controllers/groups_controller.rb +2 -21
  24. data/base/app/controllers/profiles_controller.rb +4 -0
  25. data/base/app/controllers/settings_controller.rb +12 -2
  26. data/base/app/controllers/users_controller.rb +0 -8
  27. data/base/app/helpers/contacts_helper.rb +8 -0
  28. data/base/app/models/activity.rb +19 -6
  29. data/base/app/models/actor.rb +49 -11
  30. data/base/app/models/contact.rb +12 -5
  31. data/base/app/models/group.rb +13 -11
  32. data/base/app/models/permission.rb +30 -0
  33. data/base/app/models/relation.rb +17 -7
  34. data/base/app/models/relation/custom.rb +2 -40
  35. data/base/app/models/relation/follow.rb +8 -2
  36. data/base/app/models/relation/owner.rb +10 -0
  37. data/base/app/models/relation/single.rb +7 -4
  38. data/base/app/models/site.rb +1 -1
  39. data/base/app/models/tie.rb +8 -4
  40. data/base/app/views/contacts/_add_button.html.erb +9 -0
  41. data/base/app/views/contacts/_button.html.erb +3 -3
  42. data/base/app/views/contacts/_button_multiple.html.erb +4 -0
  43. data/base/app/views/contacts/_button_simple.html.erb +17 -0
  44. data/base/app/views/contacts/_new_modal.html.erb +22 -0
  45. data/base/app/views/contacts/destroy.js.erb +5 -4
  46. data/base/app/views/contacts/update.js.erb +5 -4
  47. data/base/app/views/devise/confirmations/new.html.erb +14 -9
  48. data/base/app/views/frontpage/_presentation.html.erb +2 -2
  49. data/base/app/views/groups/_form.html.erb +20 -20
  50. data/base/app/views/groups/show.html.erb +5 -5
  51. data/base/app/views/layouts/_header_dropdown_menu.html.erb +3 -5
  52. data/base/app/views/layouts/_header_signed_in.erb +3 -3
  53. data/base/app/views/layouts/_logo_in.html.erb +1 -1
  54. data/base/app/views/layouts/application.html.erb +2 -0
  55. data/base/app/views/permissions/_list.html.erb +1 -1
  56. data/base/app/views/profiles/_avatar.html.erb +1 -5
  57. data/base/app/views/profiles/_avatar_edit.html.erb +5 -0
  58. data/base/app/views/profiles/_comunication-info.html.erb +2 -36
  59. data/base/app/views/profiles/_comunication-info_edit.html.erb +27 -0
  60. data/base/app/views/profiles/_edit_icon.html.erb +0 -1
  61. data/base/app/views/profiles/_experience.html.erb +1 -6
  62. data/base/app/views/profiles/_experience_edit.html.erb +5 -0
  63. data/base/app/views/profiles/_personal.html.erb +1 -39
  64. data/base/app/views/profiles/_personal_edit.html.erb +31 -0
  65. data/base/app/views/profiles/_profile_edit.html.erb +7 -0
  66. data/base/app/views/profiles/_tags.html.erb +1 -4
  67. data/base/app/views/profiles/_tags_edit.html.erb +4 -0
  68. data/base/app/views/profiles/edit.html.erb +24 -0
  69. data/base/app/views/settings/_notifications.html.erb +12 -4
  70. data/base/app/views/users/show.html.erb +5 -5
  71. data/base/config/locales/de.yml +654 -0
  72. data/base/config/locales/en.yml +20 -9
  73. data/base/config/locales/es.yml +151 -140
  74. data/base/config/locales/fr.yml +656 -0
  75. data/base/config/locales/hu.yml +653 -0
  76. data/base/config/locales/nl.yml +656 -0
  77. data/base/config/locales/pt.yml +250 -239
  78. data/base/config/locales/rails.de.yml +203 -0
  79. data/base/config/locales/rails.fr.yml +222 -0
  80. data/base/config/locales/rails.hu.yml +199 -0
  81. data/base/config/locales/rails.nl.yml +199 -0
  82. data/base/config/locales/zh.yml +214 -202
  83. data/base/config/routes.rb +22 -30
  84. data/base/db/migrate/20130708152633_set_group_owners.rb +19 -0
  85. data/base/db/migrate/20130723133530_actor_notification_settings.rb +10 -0
  86. data/base/lib/generators/social_stream/base/templates/initializer.rb +13 -7
  87. data/base/lib/rails/social_stream.rb +2 -0
  88. data/base/lib/social_stream/base.rb +27 -7
  89. data/base/lib/social_stream/base/ability.rb +3 -5
  90. data/base/lib/social_stream/base/autoload.rb +1 -0
  91. data/base/lib/social_stream/base/version.rb +1 -1
  92. data/base/lib/social_stream/controllers/authorship.rb +18 -0
  93. data/base/lib/social_stream/controllers/helpers.rb +18 -4
  94. data/base/lib/social_stream/controllers/objects.rb +2 -8
  95. data/base/lib/social_stream/controllers/subjects.rb +9 -2
  96. data/base/lib/social_stream/models/supertype.rb +2 -0
  97. data/base/lib/social_stream/population/activity_object.rb +6 -8
  98. data/base/lib/social_stream/routing/mapper.rb +52 -0
  99. data/base/social_stream-base.gemspec +4 -7
  100. data/base/spec/controllers/groups_controller_spec.rb +19 -8
  101. data/base/spec/controllers/posts_controller_spec.rb +114 -147
  102. data/base/spec/models/post_authorization_spec.rb +190 -204
  103. data/base/spec/models/post_spec.rb +17 -63
  104. data/base/vendor/assets/javascripts/bootstrap-multiselect.js +434 -127
  105. data/documents/Rakefile +1 -6
  106. data/documents/config/locales/de.yml +93 -0
  107. data/documents/config/locales/es.yml +2 -2
  108. data/documents/config/locales/fr.yml +93 -0
  109. data/documents/config/locales/hu.yml +93 -0
  110. data/documents/config/locales/nl.yml +93 -0
  111. data/documents/config/locales/zh.yml +4 -4
  112. data/documents/config/routes.rb +7 -9
  113. data/documents/lib/social_stream/documents/version.rb +1 -1
  114. data/documents/social_stream-documents.gemspec +4 -4
  115. data/events/Rakefile +1 -6
  116. data/events/config/locales/es.yml +3 -3
  117. data/events/config/locales/zh.yml +3 -3
  118. data/events/lib/social_stream/events/models/actor.rb +2 -1
  119. data/events/lib/social_stream/events/version.rb +1 -1
  120. data/events/social_stream-events.gemspec +1 -1
  121. data/lib/generators/social_stream/install_generator.rb +1 -0
  122. data/lib/social_stream.rb +1 -0
  123. data/lib/social_stream/version.rb +1 -1
  124. data/lib/tasks/i18n.rake +22 -2
  125. data/linkser/Rakefile +1 -6
  126. data/linkser/config/locales/de.yml +17 -0
  127. data/linkser/config/locales/es.yml +2 -2
  128. data/linkser/config/locales/fr.yml +17 -0
  129. data/linkser/config/locales/hu.yml +17 -0
  130. data/linkser/config/locales/nl.yml +17 -0
  131. data/linkser/config/locales/zh.yml +2 -2
  132. data/linkser/lib/social_stream/linkser/version.rb +1 -1
  133. data/linkser/social_stream-linkser.gemspec +1 -1
  134. data/oauth2_server/Rakefile +1 -6
  135. data/oauth2_server/app/assets/images/step_1.png +0 -0
  136. data/oauth2_server/app/assets/images/step_2.png +0 -0
  137. data/oauth2_server/app/assets/images/step_3.png +0 -0
  138. data/oauth2_server/app/assets/javascripts/social_stream/site_client.js +27 -0
  139. data/oauth2_server/app/assets/stylesheets/social_stream/oauth2_server/applications/layout/_applications-oauth2server.css.sass +0 -5
  140. data/oauth2_server/app/assets/stylesheets/social_stream/oauth2_server/create/layout/_create-oauth2server.css.sass +3 -5
  141. data/oauth2_server/app/assets/stylesheets/social_stream/oauth2_server/show/layout/_show-oauth2server.css.sass +8 -6
  142. data/oauth2_server/app/controllers/site/clients_controller.rb +17 -41
  143. data/oauth2_server/app/decorators/social_stream/base/relation_decorator.rb +2 -0
  144. data/oauth2_server/app/decorators/social_stream/base/user_decorator.rb +1 -20
  145. data/oauth2_server/app/models/relation/manager.rb +1 -10
  146. data/oauth2_server/app/models/site/client.rb +4 -2
  147. data/oauth2_server/app/views/site/clients/_destroy.html.erb +11 -0
  148. data/oauth2_server/app/views/site/clients/_edit.html.erb +9 -2
  149. data/oauth2_server/app/views/site/clients/_edit_step_2.html.erb +6 -6
  150. data/oauth2_server/app/views/site/clients/_edit_step_3.html.erb +8 -6
  151. data/oauth2_server/app/views/site/clients/_form.html.erb +11 -11
  152. data/oauth2_server/app/views/site/clients/_list.html.erb +23 -11
  153. data/oauth2_server/app/views/site/clients/edit.html.erb +1 -1
  154. data/oauth2_server/app/views/site/clients/index.html.erb +9 -40
  155. data/oauth2_server/app/views/site/clients/show.html.erb +66 -68
  156. data/oauth2_server/config/locales/en.yml +19 -0
  157. data/oauth2_server/config/locales/es.yml +23 -4
  158. data/oauth2_server/config/locales/zh.yml +32 -13
  159. data/oauth2_server/config/routes.rb +3 -1
  160. data/oauth2_server/lib/social_stream/oauth2_server.rb +4 -1
  161. data/oauth2_server/lib/social_stream/oauth2_server/ability.rb +1 -1
  162. data/oauth2_server/lib/social_stream/oauth2_server/models/user.rb +18 -0
  163. data/oauth2_server/lib/social_stream/oauth2_server/version.rb +1 -1
  164. data/oauth2_server/social_stream-oauth2_server.gemspec +1 -1
  165. data/oauth2_server/spec/controllers/site_clients_controller_authorization_spec.rb +7 -0
  166. data/ostatus/Rakefile +1 -6
  167. data/ostatus/config/locales/zh.yml +8 -8
  168. data/ostatus/lib/social_stream-ostatus.rb +3 -0
  169. data/ostatus/lib/social_stream/ostatus/version.rb +1 -1
  170. data/ostatus/social_stream-ostatus.gemspec +1 -1
  171. data/places/config/locales/es.yml +51 -51
  172. data/presence/config/locales/es.yml +48 -48
  173. data/presence/config/locales/zh.yml +48 -48
  174. data/presence/lib/social_stream/presence/version.rb +1 -1
  175. data/presence/social_stream-presence.gemspec +1 -1
  176. data/social_stream.gemspec +8 -7
  177. metadata +60 -29
  178. data/base/app/assets/javascripts/social_stream/follow.js +0 -28
  179. data/base/app/controllers/followers_controller.rb +0 -34
  180. data/base/app/helpers/followers_helper.rb +0 -5
  181. data/base/app/views/contacts/_link_follow.html.erb +0 -16
  182. data/base/app/views/followers/destroy.js.erb +0 -1
  183. data/base/app/views/followers/index.html.erb +0 -30
  184. data/base/app/views/followers/update.js.erb +0 -3
  185. data/base/app/views/frontpage/_characteristics.html.erb +0 -23
  186. data/base/lib/social_stream/routing/constraints/custom.rb +0 -11
  187. data/base/lib/social_stream/routing/constraints/follow.rb +0 -11
  188. data/base/spec/controllers/followers_controller_spec.rb +0 -37
@@ -31,78 +31,32 @@ describe Post do
31
31
  end
32
32
  end
33
33
 
34
- context "without relations" do
35
- before :all do
36
- @ss_relation_model = SocialStream.relation_model
37
- end
38
-
39
- after :all do
40
- SocialStream.relation_model = @ss_relation_model
41
- end
42
-
43
- context "in follow relation model" do
44
- before do
45
- SocialStream.relation_model = :follow
46
- end
47
-
48
- it "should allow create to follower" do
49
- tie = Factory(:follow)
50
-
51
- post = Post.new :text => "testing",
52
- :author_id => tie.receiver.id,
53
- :owner_id => tie.sender.id,
54
- :user_author_id => tie.receiver.id
55
-
56
- ability = Ability.new(tie.receiver_subject)
57
-
58
- ability.should be_able_to(:create, post)
59
- end
34
+ describe 'with default SocialStream.custom_relations' do
60
35
 
61
- it "should fill relation" do
62
- tie = Factory(:follow)
36
+ it "should allow create to friend" do
37
+ tie = Factory(:friend)
63
38
 
64
- post = Post.new :text => "testing",
65
- :author_id => tie.receiver.id,
66
- :owner_id => tie.sender.id,
67
- :user_author_id => tie.receiver.id
39
+ post = Post.new :text => "testing",
40
+ :author_id => tie.receiver.id,
41
+ :owner_id => tie.sender.id,
42
+ :user_author_id => tie.receiver.id
68
43
 
69
- post.save!
70
-
71
- post.post_activity.relations.should include(Relation::Public.instance)
72
- end
44
+ ability = Ability.new(tie.receiver_subject)
73
45
 
46
+ ability.should be_able_to(:create, post)
74
47
  end
75
48
 
76
- context "in custom relation model" do
77
- before do
78
- SocialStream.relation_model = :custom
79
- end
80
-
81
- it "should allow create to friend" do
82
- tie = Factory(:friend)
49
+ it "should fill relation" do
50
+ tie = Factory(:friend)
83
51
 
84
- post = Post.new :text => "testing",
85
- :author_id => tie.receiver.id,
86
- :owner_id => tie.sender.id,
87
- :user_author_id => tie.receiver.id
52
+ post = Post.new :text => "testing",
53
+ :author_id => tie.receiver.id,
54
+ :owner_id => tie.sender.id,
55
+ :user_author_id => tie.receiver.id
88
56
 
89
- ability = Ability.new(tie.receiver_subject)
57
+ post.save!
90
58
 
91
- ability.should be_able_to(:create, post)
92
- end
93
-
94
- it "should fill relation" do
95
- tie = Factory(:friend)
96
-
97
- post = Post.new :text => "testing",
98
- :author_id => tie.receiver.id,
99
- :owner_id => tie.sender.id,
100
- :user_author_id => tie.receiver.id
101
-
102
- post.save!
103
-
104
- post.post_activity.relations.should include(tie.relation)
105
- end
59
+ post.post_activity.relations.should include(tie.relation)
106
60
  end
107
61
 
108
62
  describe "a new post" do
@@ -16,216 +16,523 @@
16
16
  * See the License for the specific language governing permissions and
17
17
  * limitations under the License.
18
18
  */
19
- !function ($) {
19
+ !function($) {"use strict";// jshint ;_;
20
20
 
21
- "use strict"; // jshint ;_;
22
-
23
- if(typeof ko != 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect){
21
+ if ( typeof ko != 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect) {
24
22
  ko.bindingHandlers.multiselect = {
25
- init: function (element) {
23
+ init : function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
24
+ },
25
+ update : function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
26
26
  var ms = $(element).data('multiselect');
27
-
28
- if(!ms)
29
- throw new Error("Bootstrap-multiselect's multiselect() has to be called on element before applying the Knockout View model!");
30
-
31
- var prev = ms.options.onChange;
32
-
33
- ms.options.onChange = function(option, checked){
34
- // We dont want to refresh the multiselect since it would delete / recreate all items
35
- $(element).data('blockRefresh', true);
36
-
37
- // Force the binding to be updated by triggering the change event on the select element
38
- $(element).trigger('change');
39
-
40
- // Call any defined change handler
41
- return prev(option, checked);
27
+ if (!ms) {
28
+ $(element).multiselect(ko.utils.unwrapObservable(valueAccessor()));
29
+ }
30
+ else
31
+ if (allBindingsAccessor().options && allBindingsAccessor().options().length !== ms.originalOptions.length) {
32
+ ms.updateOriginalOptions();
33
+ $(element).multiselect('rebuild');
42
34
  }
43
- },
44
- update: function (element) {
45
- var blockRefresh = $(element).data('blockRefresh') || false;
46
- if (!blockRefresh) { $(element).multiselect("rebuild"); }
47
- $.data(element, 'blockRefresh', false);
48
35
  }
49
36
  };
50
37
  }
51
38
 
52
39
  function Multiselect(select, options) {
53
-
54
- this.options = this.getOptions(options);
55
- this.select = $(select);
56
-
57
- // Manually add the multiple attribute, if its not already set.
58
- if (!this.select.attr('multiple')) {
59
- this.select.attr('multiple', true);
60
- }
61
-
62
- this.container = $(this.options.buttonContainer)
63
- .append('<button type="button" class="multiselect dropdown-toggle ' + this.options.buttonClass + '" data-toggle="dropdown">' + this.options.buttonText($('option:selected', select)) + '</button>')
64
- .append('<ul class="dropdown-menu"></ul>');
65
40
 
66
- if (this.options.buttonWidth) {
67
- $('button', this.container).css({
68
- 'width': this.options.buttonWidth
69
- });
70
- }
41
+ this.options = this.getOptions(options);
42
+ this.$select = $(select);
43
+ this.originalOptions = this.$select.clone()[0].options;
44
+ //we have to clone to create a new reference
45
+ this.query = '';
46
+ this.searchTimeout = null;
47
+
48
+ this.options.multiple = this.$select.attr('multiple') == "multiple";
49
+
50
+ this.$container = $(this.options.buttonContainer).append('<button type="button" class="multiselect dropdown-toggle ' + this.options.buttonClass + '" data-toggle="dropdown">' + this.options.buttonText(this.getSelected(), this.$select) + '</button>')
51
+ .append('<ul class="multiselect-container dropdown-menu' + (this.options.dropRight ? ' pull-right' : '') + '"></ul>');
52
+
53
+ if (this.options.buttonWidth) {
54
+ $('button', this.$container).css({
55
+ 'width' : this.options.buttonWidth
56
+ });
57
+ }
71
58
 
72
59
  // Set max height of dropdown menu to activate auto scrollbar.
73
60
  if (this.options.maxHeight) {
74
- $('ul', this.container).css({
75
- 'max-height': this.options.maxHeight + 'px',
76
- 'overflow-y': 'auto',
77
- 'overflow-x': 'hidden'
61
+ // TODO: Add a class for this option to move the css declarations.
62
+ $('.multiselect-container', this.$container).css({
63
+ 'max-height' : this.options.maxHeight + 'px',
64
+ 'overflow-y' : 'auto',
65
+ 'overflow-x' : 'hidden'
78
66
  });
79
67
  }
80
68
 
81
- this.buildDrowdown(select, this.options);
69
+ // Enable filtering.
70
+ if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) {
71
+ this.buildFilter();
72
+ }
73
+
74
+ this.buildDropdown();
75
+ this.updateButtonText();
82
76
 
83
- this.select
84
- .hide()
85
- .after(this.container);
77
+ this.$select.hide().after(this.$container);
86
78
  };
87
79
 
88
80
  Multiselect.prototype = {
89
-
90
- defaults: {
91
- // Default text function will either print 'None selected' in case no option is selected,
92
- // or a list of the selected options up to a length of 3 selected options.
81
+
82
+ defaults : {
83
+ // Default text function will either print 'None selected' in case no
84
+ // option is selected, or a list of the selected options up to a length of 3 selected options.
93
85
  // If more than 3 options are selected, the number of selected options is printed.
94
- buttonText: function(options) {
86
+ buttonText : function(options, select) {
95
87
  if (options.length == 0) {
96
- return 'None selected <b class="caret"></b>';
88
+ return this.nonSelectedText + '<b class="caret"></b>';
97
89
  }
98
- else if (options.length > 3) {
99
- return options.length + ' selected <b class="caret"></b>';
90
+ else
91
+ if (options.length > 3) {
92
+ return options.length + ' ' + this.nSelectedText + ' <b class="caret"></b>';
100
93
  }
101
94
  else {
102
95
  var selected = '';
103
96
  options.each(function() {
104
- selected += $(this).text() + ', ';
97
+ var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).html();
98
+
99
+ selected += label + ', ';
105
100
  });
106
- return selected.substr(0, selected.length -2) + ' <b class="caret"></b>';
101
+ return selected.substr(0, selected.length - 2) + ' <b class="caret"></b>';
107
102
  }
108
103
  },
109
104
  // Is triggered on change of the selected options.
110
- onChange: function() {
105
+ onChange : function(option, checked) {
111
106
 
112
107
  },
113
- buttonClass: 'btn',
114
- buttonWidth: 'auto',
115
- buttonContainer: '<div class="btn-group" />',
116
- // Maximum height of thet dropdown menu.
108
+ buttonClass : 'btn',
109
+ dropRight : false,
110
+ selectedClass : 'active',
111
+ buttonWidth : 'auto',
112
+ buttonContainer : '<div class="btn-group" />',
113
+ // Maximum height of the dropdown menu.
117
114
  // If maximum height is exceeded a scrollbar will be displayed.
118
- maxHeight: 400
115
+ maxHeight : false,
116
+ includeSelectAllOption : false,
117
+ selectAllText : ' Select all',
118
+ selectAllValue : 'multiselect-all',
119
+ enableFiltering : false,
120
+ enableCaseInsensitiveFiltering : false,
121
+ filterPlaceholder : 'Search',
122
+ // possible options: 'text', 'value', 'both'
123
+ filterBehavior : 'text',
124
+ preventInputChangeEvent: false,
125
+ nonSelectedText: 'None selected',
126
+ nSelectedText: 'selected'
119
127
  },
120
-
121
- isMobile: function() {
122
- return navigator.userAgent.match(/Android|webOS|iPhone|iPad|iPod|BlackBerry/i);
123
- },
124
128
 
125
- constructor: Multiselect,
129
+ constructor : Multiselect,
126
130
 
127
- buildDrowdown: function(select, options){
131
+ // Will build an dropdown element for the given option.
132
+ createOptionValue : function(element) {
133
+ if ($(element).is(':selected')) {
134
+ $(element).attr('selected', 'selected').prop('selected', true);
135
+ }
128
136
 
129
- // Build the dropdown.
130
- $('option', this.select).each($.proxy(function(index, element) {
131
- if ($(element).is(':selected')) {
132
- $(element).attr('selected', 'selected');
133
- $(element).prop('selected', 'selected');
134
- }
137
+ // Support the label attribute on options.
138
+ var label = $(element).attr('label') || $(element).html();
139
+ var value = $(element).val();
140
+ var inputType = this.options.multiple ? "checkbox" : "radio";
135
141
 
136
- $('ul', this.container).append('<li><a href="javascript:void(0);" style="padding:0;"><label style="margin:0;padding:3px 20px 3px 20px;width:100%;height:100%;cursor:pointer;"><input style="margin-bottom:5px;" type="checkbox" value="' + $(element).val() + '" /> ' + $(element).text() + '</label</a></li>');
142
+ var $li = $('<li><a href="javascript:void(0);"><label class="' + inputType + '"><input type="' + inputType + '" /></label></a></li>');
137
143
 
138
- var selected = $(element).prop('selected') || false;
139
- var checkbox = $('ul li input[value="' + $(element).val() + '"]', this.container);
140
-
141
- if ($(element).is(':disabled')) {
142
- checkbox.attr('disabled', 'disabled').prop('disabled','disabled').parents('li').addClass('disabled')
143
- }
144
-
145
- checkbox.prop('checked', selected);
144
+ var selected = $(element).prop('selected') || false;
145
+ var $checkbox = $('input', $li);
146
+ $checkbox.val(value);
147
+
148
+ if (value == this.options.selectAllValue) {
149
+ $checkbox.parent().parent().addClass('multiselect-all');
150
+ }
151
+
152
+ $('label', $li).append(" " + label);
153
+
154
+ $('.multiselect-container', this.$container).append($li);
155
+
156
+ if ($(element).is(':disabled')) {
157
+ $checkbox.attr('disabled', 'disabled').prop('disabled', true).parents('li').addClass('disabled');
158
+ }
159
+
160
+ $checkbox.prop('checked', selected);
161
+
162
+ if (selected && this.options.selectedClass) {
163
+ $checkbox.parents('li').addClass(this.options.selectedClass);
164
+ }
165
+ },
166
+
167
+ toggleActiveState : function(shouldBeActive) {
168
+ if (this.$select.attr('disabled') == undefined) {
169
+ $('button.multiselect.dropdown-toggle', this.$container).removeClass('disabled');
170
+ }
171
+ else {
172
+ $('button.multiselect.dropdown-toggle', this.$container).addClass('disabled');
173
+ }
174
+ },
175
+
176
+ // Build the dropdown and bind event handling.
177
+ buildDropdown : function() {
178
+ var alreadyHasSelectAll = this.$select[0][0] ? this.$select[0][0].value == this.options.selectAllValue : false;
179
+
180
+ // If options.includeSelectAllOption === true, add the include all
181
+ // checkbox.
182
+ if (this.options.includeSelectAllOption && this.options.multiple && !alreadyHasSelectAll) {
183
+ this.$select.prepend('<option value="' + this.options.selectAllValue + '">' + this.options.selectAllText + '</option>');
184
+ }
185
+
186
+ this.toggleActiveState();
187
+
188
+ this.$select.children().each($.proxy(function(index, element) {
189
+ // Support optgroups and options without a group simultaneously.
190
+ var tag = $(element).prop('tagName').toLowerCase();
191
+ if (tag == 'optgroup') {
192
+ var group = element;
193
+ var groupName = $(group).prop('label');
146
194
 
147
- if (selected) {
148
- checkbox.parents('li').addClass('active');
195
+ // Add a header for the group.
196
+ var $li = $('<li><label class="multiselect-group"></label></li>');
197
+ $('label', $li).text(groupName);
198
+ $('.multiselect-container', this.$container).append($li);
199
+
200
+ // Add the options of the group.
201
+ $('option', group).each($.proxy(function(index, element) {
202
+ this.createOptionValue(element);
203
+ }, this));
204
+ }
205
+ else
206
+ if (tag == 'option') {
207
+ this.createOptionValue(element);
208
+ }
209
+ else {
210
+ // Ignore illegal tags.
149
211
  }
150
212
  }, this));
151
213
 
152
214
  // Bind the change event on the dropdown elements.
153
- $('ul li input[type="checkbox"]', this.container).on('change', $.proxy(function(event) {
215
+ $('.multiselect-container li input', this.$container).on('change', $.proxy(function(event) {
154
216
  var checked = $(event.target).prop('checked') || false;
217
+ var isSelectAllOption = $(event.target).val() == this.options.selectAllValue;
218
+
219
+ // Apply or unapply the configured selected class.
220
+ if (this.options.selectedClass) {
221
+ if (checked) {
222
+ $(event.target).parents('li').addClass(this.options.selectedClass);
223
+ }
224
+ else {
225
+ $(event.target).parents('li').removeClass(this.options.selectedClass);
226
+ }
227
+ }
228
+
229
+ var $option = $('option', this.$select).filter(function() {
230
+ return $(this).val() == $(event.target).val();
231
+ });
232
+
233
+ var $optionsNotThis = $('option', this.$select).not($option);
234
+ var $checkboxesNotThis = $('input', this.$container).not($(event.target));
235
+
236
+ // Toggle all options if the select all option was changed.
237
+ if (isSelectAllOption) {
238
+ $checkboxesNotThis.filter(function() {
239
+ return $(this).is(':checked') != checked;
240
+ }).trigger('click');
241
+ }
155
242
 
156
243
  if (checked) {
157
- $(event.target).parents('li').addClass('active');
244
+ $option.prop('selected', true);
245
+
246
+ if (this.options.multiple) {
247
+ $option.attr('selected', 'selected');
248
+ }
249
+ else {
250
+ if (this.options.selectedClass) {
251
+ $($checkboxesNotThis).parents('li').removeClass(this.options.selectedClass);
252
+ }
253
+
254
+ $($checkboxesNotThis).prop('checked', false);
255
+
256
+ $optionsNotThis.removeAttr('selected').prop('selected', false);
257
+
258
+ // It's a single selection, so close.
259
+ $(this.$container).find(".multiselect.dropdown-toggle").click();
260
+ }
261
+
262
+ if (this.options.selectedClass == "active") {
263
+ $optionsNotThis.parents("a").css("outline", "");
264
+ }
265
+
158
266
  }
159
267
  else {
160
- $(event.target).parents('li').removeClass('active');
268
+ $option.removeAttr('selected').prop('selected', false);
161
269
  }
162
270
 
163
- var option = $('option[value="' + $(event.target).val() + '"]', this.select);
271
+ this.updateButtonText();
164
272
 
165
- if (checked) {
166
- option.attr('selected', 'selected');
167
- option.prop('selected', 'selected');
273
+ this.options.onChange($option, checked);
274
+
275
+ this.$select.change();
276
+
277
+ if(this.options.preventInputChangeEvent) {
278
+ return false;
279
+ }
280
+ }, this));
281
+
282
+ $('.multiselect-container li a', this.$container).on('touchstart click', function(event) {
283
+ event.stopPropagation();
284
+ $(event.target).blur();
285
+ });
286
+
287
+ // Keyboard support.
288
+ this.$container.on('keydown', $.proxy(function(event) {
289
+ if ($('input[type="text"]', this.$container).is(':focus'))
290
+ return;
291
+ if ((event.keyCode == 9 || event.keyCode == 27) && this.$container.hasClass('open')) {
292
+ // Close on tab or escape.
293
+ $(this.$container).find(".multiselect.dropdown-toggle").click();
168
294
  }
169
295
  else {
170
- option.removeAttr('selected');
296
+ var $items = $(this.$container).find("li:not(.divider):visible a");
297
+
298
+ if (!$items.length) {
299
+ return;
300
+ }
301
+
302
+ var index = $items.index($items.filter(':focus'));
303
+
304
+ // Navigation up.
305
+ if (event.keyCode == 38 && index > 0) {
306
+ index--;
307
+ }
308
+ // Navigate down.
309
+ else
310
+ if (event.keyCode == 40 && index < $items.length - 1) {
311
+ index++;
312
+ }
313
+ else
314
+ if (!~index) {
315
+ index = 0;
316
+ }
317
+
318
+ var $current = $items.eq(index);
319
+ $current.focus();
320
+
321
+ // Override style for items in li:active.
322
+ if (this.options.selectedClass == "active") {
323
+ $current.css("outline", "thin dotted #333").css("outline", "5px auto -webkit-focus-ring-color");
324
+
325
+ $items.not($current).css("outline", "");
326
+ }
327
+
328
+ if (event.keyCode == 32 || event.keyCode == 13) {
329
+ var $checkbox = $current.find('input');
330
+
331
+ $checkbox.prop("checked", !$checkbox.prop("checked"));
332
+ $checkbox.change();
333
+ }
334
+
335
+ event.stopPropagation();
336
+ event.preventDefault();
171
337
  }
172
-
173
- var options = $('option:selected', this.select);
174
- $('button', this.container).html(this.options.buttonText(options));
175
-
176
- this.options.onChange(option, checked);
177
338
  }, this));
339
+ },
340
+
341
+ // Build and bind filter.
342
+ buildFilter: function() {
343
+ $('.multiselect-container', this.$container).prepend('<div class="input-prepend"><span class="add-on"><i class="icon-search"></i></span><input class="multiselect-search" type="text" placeholder="' + this.options.filterPlaceholder + '"></div>');
178
344
 
179
- $('ul li a', this.container).on('click', function(event) {
345
+ $('.multiselect-search', this.$container).val(this.query).on('click', function(event) {
180
346
  event.stopPropagation();
181
- });
347
+ }).on('keydown', $.proxy(function(event) {
348
+ // This is useful to catch "keydown" events after the browser has
349
+ // updated the control.
350
+ clearTimeout(this.searchTimeout);
351
+
352
+ this.searchTimeout = this.asyncFunction($.proxy(function() {
353
+
354
+ if (this.query != event.target.value) {
355
+ this.query = event.target.value;
356
+
357
+ $.each($('.multiselect-container li', this.$container), $.proxy(function(index, element) {
358
+ var value = $('input', element).val();
359
+ if (value != this.options.selectAllValue) {
360
+ var text = $('label', element).text();
361
+ var value = $('input', element).val();
362
+ if (value && text && value != this.options.selectAllValue) {
363
+ // by default lets assume that element is not
364
+ // interesting for this search
365
+ var showElement = false;
366
+
367
+ var filterCandidate = '';
368
+ if ((this.options.filterBehavior == 'text' || this.options.filterBehavior == 'both')) {
369
+ filterCandidate = text;
370
+ }
371
+ if ((this.options.filterBehavior == 'value' || this.options.filterBehavior == 'both')) {
372
+ filterCandidate = value;
373
+ }
374
+
375
+ if (this.options.enableCaseInsensitiveFiltering && filterCandidate.toLowerCase().indexOf(this.query.toLowerCase()) > -1) {
376
+ showElement = true;
377
+ }
378
+ else if (filterCandidate.indexOf(this.query) > -1) {
379
+ showElement = true;
380
+ }
381
+
382
+ if (showElement) {
383
+ $(element).show();
384
+ }
385
+ else {
386
+ $(element).hide();
387
+ }
388
+ }
389
+ }
390
+ }, this));
391
+ }
392
+ }, this), 300, this);
393
+ }, this));
182
394
  },
183
395
 
184
396
  // Destroy - unbind - the plugin.
185
- destroy: function() {
186
- this.container.remove();
187
- this.select.show();
397
+ destroy : function() {
398
+ this.$container.remove();
399
+ this.$select.show();
188
400
  },
189
401
 
190
402
  // Refreshs the checked options based on the current state of the select.
191
- refresh: function() {
192
- $('option', this.select).each($.proxy(function(index, element) {
403
+ refresh : function() {
404
+ $('option', this.$select).each($.proxy(function(index, element) {
405
+ var $input = $('.multiselect-container li input', this.$container).filter(function() {
406
+ return $(this).val() == $(element).val();
407
+ });
408
+
193
409
  if ($(element).is(':selected')) {
194
- $('ul li input[value="' + $(element).val() + '"]', this.container).prop('checked', true);
195
- $('ul li input[value="' + $(element).val() + '"]', this.container).parents('li').addClass('active');
410
+ $input.prop('checked', true);
411
+
412
+ if (this.options.selectedClass) {
413
+ $input.parents('li').addClass(this.options.selectedClass);
414
+ }
415
+ }
416
+ else {
417
+ $input.prop('checked', false);
418
+
419
+ if (this.options.selectedClass) {
420
+ $input.parents('li').removeClass(this.options.selectedClass);
421
+ }
422
+ }
423
+
424
+ if ($(element).is(":disabled")) {
425
+ $input.attr('disabled', 'disabled').prop('disabled', true).parents('li').addClass('disabled');
196
426
  }
197
427
  else {
198
- $('ul li input[value="' + $(element).val() + '"]', this.container).prop('checked', false);
199
- $('ul li input[value="' + $(element).val() + '"]', this.container).parents('li').removeClass('active');
428
+ $input.removeAttr('disabled').prop('disabled', false).parents('li').removeClass('disabled');
200
429
  }
201
430
  }, this));
202
431
 
203
- $('button', this.container).html(this.options.buttonText($('option:selected', this.select)));
432
+ this.updateButtonText();
433
+ },
434
+
435
+ // Select an option by its value.
436
+ select : function(value) {
437
+ var $option = $('option', this.$select).filter(function() {
438
+ return $(this).val() == value;
439
+ });
440
+ var $checkbox = $('.multiselect-container li input', this.$container).filter(function() {
441
+ return $(this).val() == value;
442
+ });
443
+
444
+ if (this.options.selectedClass) {
445
+ $checkbox.parents('li').addClass(this.options.selectedClass);
446
+ }
447
+
448
+ $checkbox.prop('checked', true);
449
+
450
+ $option.attr('selected', 'selected').prop('selected', true);
451
+
452
+ this.updateButtonText();
453
+ this.options.onChange($option, true);
454
+ },
455
+
456
+ // Deselect an option by its value.
457
+ deselect : function(value) {
458
+ var $option = $('option', this.$select).filter(function() {
459
+ return $(this).val() == value;
460
+ });
461
+ var $checkbox = $('.multiselect-container li input', this.$container).filter(function() {
462
+ return $(this).val() == value;
463
+ });
464
+
465
+ if (this.options.selectedClass) {
466
+ $checkbox.parents('li').removeClass(this.options.selectedClass);
467
+ }
468
+
469
+ $checkbox.prop('checked', false);
470
+
471
+ $option.removeAttr('selected').prop('selected', false);
472
+
473
+ this.updateButtonText();
474
+ this.options.onChange($option, false);
204
475
  },
205
476
 
206
- rebuild: function() {
207
- $('ul', this.container).html('');
208
- this.buildDrowdown(this.select, this.options);
209
- },
477
+ // Rebuild the whole dropdown menu.
478
+ rebuild : function() {
479
+ $('.multiselect-container', this.$container).html('');
480
+ this.buildDropdown(this.$select, this.options);
481
+ this.updateButtonText();
482
+
483
+ // Enable filtering.
484
+ if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) {
485
+ this.buildFilter();
486
+ }
487
+ },
210
488
 
211
489
  // Get options by merging defaults and given options.
212
- getOptions: function(options) {
490
+ getOptions : function(options) {
213
491
  return $.extend({}, this.defaults, options);
492
+ },
493
+
494
+ updateButtonText : function() {
495
+ var options = this.getSelected();
496
+ $('button', this.$container).html(this.options.buttonText(options, this.$select));
497
+ },
498
+
499
+ // Get all selected options.
500
+ getSelected : function() {
501
+ return $('option:selected[value!="' + this.options.selectAllValue + '"]', this.$select);
502
+ },
503
+
504
+ updateOriginalOptions : function() {
505
+ this.originalOptions = this.$select.clone()[0].options;
506
+ },
507
+
508
+ asyncFunction : function(callback, timeout, self) {
509
+ var args = Array.prototype.slice.call(arguments, 3);
510
+ return setTimeout(function() {
511
+ callback.apply(self || window, args);
512
+ }, timeout);
214
513
  }
215
514
  };
216
515
 
217
- $.fn.multiselect = function (option) {
218
- return this.each(function () {
219
- var data = $(this).data('multiselect'),
220
- options = typeof option == 'object' && option;
516
+ $.fn.multiselect = function(option, parameter) {
517
+ return this.each(function() {
518
+ var data = $(this).data('multiselect'), options = typeof option == 'object' && option;
221
519
 
520
+ // Initialize the multiselect.
222
521
  if (!data) {
223
- $(this).data('multiselect', (data = new Multiselect(this, options)));
522
+ $(this).data('multiselect', ( data = new Multiselect(this, options)));
224
523
  }
225
524
 
226
- if (typeof option == 'string') {
227
- data[option]();
525
+ // Call multiselect method.
526
+ if ( typeof option == 'string') {
527
+ data[option](parameter);
228
528
  }
229
529
  });
230
- }
530
+ };
531
+
532
+ $.fn.multiselect.Constructor = Multiselect;
533
+
534
+ $(function() {
535
+ $("select[data-role=multiselect]").multiselect();
536
+ });
537
+
231
538
  }(window.jQuery);