decidim-core 0.10.1 → 0.11.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (269) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -11
  3. data/app/assets/images/decidim/icons.svg +25 -6
  4. data/app/assets/javascripts/decidim/account_form.js.es6 +8 -8
  5. data/app/assets/javascripts/decidim/append_elements.js.es6 +1 -1
  6. data/app/assets/javascripts/decidim/append_redirect_url_to_modals.js.es6 +5 -5
  7. data/app/assets/javascripts/decidim/data_picker.js.es6 +10 -10
  8. data/app/assets/javascripts/decidim/editor.js.es6 +37 -22
  9. data/app/assets/javascripts/decidim/filters.js.es6 +1 -1
  10. data/app/assets/javascripts/decidim/form_filter.component.js.es6 +15 -15
  11. data/app/assets/javascripts/decidim/form_filter.component.test.js +29 -29
  12. data/app/assets/javascripts/decidim/impersonation.js.es6 +3 -3
  13. data/app/assets/javascripts/decidim/input_mentions.js.es6 +100 -0
  14. data/app/assets/javascripts/decidim/input_tags.js.es6 +12 -0
  15. data/app/assets/javascripts/decidim/{map.js.es6.erb → map.js.es6} +9 -9
  16. data/app/assets/javascripts/decidim/notifications.js.es6 +10 -10
  17. data/app/assets/javascripts/decidim/orders.js.es6 +5 -5
  18. data/app/assets/javascripts/decidim/user_registrations.js.es6 +4 -4
  19. data/app/assets/javascripts/decidim/widget.js.es6 +1 -1
  20. data/app/assets/javascripts/decidim.js.es6 +10 -0
  21. data/app/assets/stylesheets/decidim/application.scss.erb +1 -1
  22. data/app/assets/stylesheets/decidim/modules/_author-avatar.scss +39 -0
  23. data/app/assets/stylesheets/decidim/modules/_cards.scss +158 -3
  24. data/app/assets/stylesheets/decidim/modules/_definition-data.scss +6 -0
  25. data/app/assets/stylesheets/decidim/modules/_extra.scss +1 -3
  26. data/app/assets/stylesheets/decidim/modules/_icons.scss +14 -6
  27. data/app/assets/stylesheets/decidim/modules/_inline-filters.scss +61 -0
  28. data/app/assets/stylesheets/decidim/modules/_input-mentions.scss +124 -0
  29. data/app/assets/stylesheets/decidim/modules/_input-tags.scss +55 -0
  30. data/app/assets/stylesheets/decidim/modules/_modules.scss +3 -0
  31. data/app/assets/stylesheets/decidim/modules/_status-labels.scss +4 -0
  32. data/app/assets/stylesheets/decidim/modules/_typography.scss +16 -0
  33. data/app/assets/stylesheets/decidim/utils/_helpers.scss +29 -0
  34. data/app/assets/stylesheets/decidim/utils/_mixins.scss +6 -0
  35. data/app/cells/decidim/author_box/show.erb +10 -0
  36. data/app/cells/decidim/author_box_cell.rb +21 -0
  37. data/app/cells/decidim/card/show.erb +17 -0
  38. data/app/cells/decidim/card_cell.rb +29 -0
  39. data/app/cells/decidim/profile/profile_inline.erb +21 -0
  40. data/app/cells/decidim/profile/show.erb +13 -0
  41. data/app/cells/decidim/profile_cell.rb +17 -0
  42. data/app/commands/decidim/create_omniauth_registration.rb +3 -2
  43. data/app/commands/decidim/create_report.rb +1 -1
  44. data/app/commands/decidim/invite_user.rb +2 -0
  45. data/app/constraints/decidim/current_component.rb +41 -0
  46. data/app/controllers/concerns/decidim/action_authorization.rb +3 -3
  47. data/app/controllers/concerns/decidim/devise_controllers.rb +2 -1
  48. data/app/controllers/concerns/decidim/filter_resource.rb +1 -1
  49. data/app/controllers/concerns/decidim/form_factory.rb +1 -1
  50. data/app/controllers/concerns/decidim/impersonate_users.rb +6 -2
  51. data/app/controllers/concerns/decidim/needs_authorization.rb +2 -2
  52. data/app/controllers/concerns/decidim/participatory_space_context.rb +15 -0
  53. data/app/controllers/concerns/decidim/settings.rb +5 -5
  54. data/app/controllers/decidim/application_controller.rb +2 -1
  55. data/app/controllers/decidim/{features → components}/base_controller.rb +16 -8
  56. data/app/controllers/decidim/devise/omniauth_registrations_controller.rb +3 -1
  57. data/app/controllers/decidim/devise/registrations_controller.rb +1 -3
  58. data/app/controllers/decidim/doorkeeper/authorizations_controller.rb +16 -0
  59. data/app/controllers/decidim/doorkeeper/credentials_controller.rb +46 -0
  60. data/app/controllers/decidim/doorkeeper/token_info_controller.rb +9 -0
  61. data/app/controllers/decidim/doorkeeper/tokens_controller.rb +9 -0
  62. data/app/controllers/decidim/widgets_controller.rb +1 -1
  63. data/app/forms/decidim/follow_form.rb +1 -1
  64. data/app/forms/decidim/form.rb +1 -1
  65. data/app/forms/decidim/omniauth_registration_form.rb +1 -0
  66. data/app/forms/decidim/registration_form.rb +5 -5
  67. data/app/helpers/decidim/card_helper.rb +16 -0
  68. data/app/helpers/decidim/component_path_helper.rb +36 -0
  69. data/app/helpers/decidim/icon_helper.rb +7 -7
  70. data/app/helpers/decidim/messaging/conversation_helper.rb +4 -3
  71. data/app/helpers/decidim/paginate_helper.rb +1 -1
  72. data/app/helpers/decidim/resource_helper.rb +2 -2
  73. data/app/jobs/decidim/export_job.rb +3 -3
  74. data/app/mailers/decidim/messaging/conversation_mailer.rb +0 -2
  75. data/app/middleware/decidim/current_organization.rb +2 -2
  76. data/app/models/decidim/abilities/admin_ability.rb +1 -1
  77. data/app/models/decidim/abilities/everyone_ability.rb +1 -1
  78. data/app/models/decidim/abilities/participatory_process_admin_ability.rb +2 -2
  79. data/app/models/decidim/abilities/participatory_process_collaborator_ability.rb +2 -2
  80. data/app/models/decidim/action_log.rb +7 -5
  81. data/app/models/decidim/area.rb +7 -0
  82. data/app/models/decidim/authorization.rb +14 -0
  83. data/app/models/decidim/{feature.rb → component.rb} +15 -15
  84. data/app/models/decidim/moderation.rb +1 -1
  85. data/app/models/decidim/oauth_application.rb +24 -0
  86. data/app/models/decidim/organization.rb +5 -0
  87. data/app/models/decidim/participatory_space_link.rb +20 -0
  88. data/app/models/decidim/participatory_space_private_user.rb +19 -0
  89. data/app/models/decidim/user.rb +7 -0
  90. data/app/presenters/decidim/admin_log/area_presenter.rb +38 -0
  91. data/app/presenters/decidim/admin_log/{feature_presenter.rb → component_presenter.rb} +5 -5
  92. data/app/presenters/decidim/admin_log/newsletter_resource_presenter.rb +1 -1
  93. data/app/presenters/decidim/admin_log/oauth_application_presenter.rb +50 -0
  94. data/app/presenters/decidim/admin_log/oauth_application_resource_presenter.rb +18 -0
  95. data/app/presenters/decidim/home_stats_presenter.rb +7 -7
  96. data/app/presenters/decidim/log/value_types/area_presenter.rb +1 -1
  97. data/app/presenters/decidim/log/value_types/area_type_presenter.rb +28 -0
  98. data/app/presenters/decidim/log/value_types/currency_presenter.rb +20 -0
  99. data/app/presenters/decidim/log/value_types/scope_presenter.rb +1 -1
  100. data/app/presenters/decidim/log/value_types/scope_type_presenter.rb +1 -1
  101. data/app/presenters/decidim/resource_locator_presenter.rb +3 -3
  102. data/app/services/decidim/action_authorizer.rb +9 -9
  103. data/app/services/decidim/action_logger.rb +9 -7
  104. data/app/services/decidim/email_notification_generator.rb +1 -1
  105. data/app/services/decidim/notification_generator_for_recipient.rb +1 -1
  106. data/app/services/decidim/resource_search.rb +8 -8
  107. data/app/services/decidim/settings_change.rb +5 -5
  108. data/app/services/decidim/static_map_generator.rb +1 -1
  109. data/app/types/decidim/core/attachment_type.rb +14 -0
  110. data/app/types/decidim/core/category_type.rb +16 -0
  111. data/app/types/decidim/core/coordinates_type.rb +19 -0
  112. data/app/types/decidim/core/scope_api_type.rb +16 -0
  113. data/app/uploaders/decidim/oauth_application_logo_uploader.rb +9 -0
  114. data/app/validators/geocoding_validator.rb +2 -2
  115. data/app/views/decidim/account/delete.html.erb +7 -7
  116. data/app/views/decidim/application/_collection.html.erb +1 -1
  117. data/app/views/decidim/application/_document.html.erb +1 -1
  118. data/app/views/decidim/application/_photos.html.erb +1 -1
  119. data/app/views/decidim/devise/invitations/edit.html.erb +1 -1
  120. data/app/views/decidim/devise/omniauth_registrations/new.html.erb +1 -1
  121. data/app/views/decidim/devise/registrations/new.html.erb +1 -1
  122. data/app/views/decidim/devise/sessions/new.html.erb +2 -2
  123. data/app/views/decidim/devise/shared/_omniauth_buttons.html.erb +1 -1
  124. data/app/views/decidim/devise/shared/_omniauth_buttons_mini.html.erb +1 -1
  125. data/app/views/decidim/doorkeeper/authorizations/new.html.erb +58 -0
  126. data/app/views/decidim/messaging/conversations/_show.html.erb +1 -1
  127. data/app/views/decidim/messaging/conversations/index.html.erb +1 -1
  128. data/app/views/decidim/notifications/index.html.erb +1 -1
  129. data/app/views/decidim/notifications_settings/show.html.erb +2 -2
  130. data/app/views/decidim/own_user_groups/index.html.erb +3 -3
  131. data/app/views/decidim/scopes/_scopes_picker_input.html.erb +4 -4
  132. data/app/views/decidim/scopes/picker.html.erb +3 -3
  133. data/app/views/decidim/shared/_action_authorization_modal.html.erb +1 -1
  134. data/app/views/decidim/shared/_author.html.erb +1 -1
  135. data/app/views/decidim/shared/_author_reference.html.erb +1 -12
  136. data/app/views/decidim/shared/{_feature_announcement.html.erb → _component_announcement.html.erb} +2 -2
  137. data/app/views/decidim/shared/_embed_modal.html.erb +1 -1
  138. data/app/views/decidim/shared/_flag_modal.html.erb +5 -5
  139. data/app/views/decidim/shared/_login_modal.html.erb +3 -3
  140. data/app/views/decidim/shared/_private_participatory_space.html.erb +5 -0
  141. data/app/views/decidim/shared/_share_modal.html.erb +1 -1
  142. data/app/views/decidim/shared/_tags.html.erb +1 -1
  143. data/app/views/decidim/shared/_version_author.html.erb +1 -1
  144. data/app/views/decidim/widgets/_data_picker.html.erb +4 -4
  145. data/app/views/devise/mailer/confirmation_instructions.html.erb +3 -3
  146. data/app/views/devise/mailer/invite_private_user.html.erb +17 -0
  147. data/app/views/devise/mailer/invite_private_user.text.erb +15 -0
  148. data/app/views/devise/mailer/password_change.html.erb +2 -2
  149. data/app/views/devise/mailer/reset_password_instructions.html.erb +5 -5
  150. data/app/views/kaminari/decidim/_first_page.html.erb +2 -3
  151. data/app/views/kaminari/decidim/_gap.html.erb +2 -3
  152. data/app/views/kaminari/decidim/_last_page.html.erb +2 -3
  153. data/app/views/kaminari/decidim/_next_page.html.erb +2 -3
  154. data/app/views/kaminari/decidim/_page.html.erb +2 -3
  155. data/app/views/kaminari/decidim/_paginator.html.erb +1 -2
  156. data/app/views/kaminari/decidim/_prev_page.html.erb +2 -3
  157. data/app/views/layouts/decidim/_application.html.erb +4 -4
  158. data/app/views/layouts/decidim/_component_authorization_modals.html.erb +5 -0
  159. data/app/views/layouts/decidim/_cookie_warning.html.erb +2 -2
  160. data/app/views/layouts/decidim/_head.html.erb +4 -4
  161. data/app/views/layouts/decidim/_impersonation_warning.html.erb +4 -4
  162. data/app/views/layouts/decidim/_language_chooser.html.erb +1 -1
  163. data/app/views/layouts/decidim/_social_media_links.html.erb +5 -5
  164. data/app/views/layouts/decidim/_wrapper.html.erb +5 -5
  165. data/app/views/layouts/decidim/mailer.html.erb +1 -1
  166. data/app/views/layouts/decidim/widget.html.erb +5 -5
  167. data/app/views/pages/home/_hero.html.erb +1 -1
  168. data/app/views/pages/home.html.erb +6 -6
  169. data/config/locales/ca.yml +66 -27
  170. data/config/locales/en.yml +69 -30
  171. data/config/locales/es.yml +66 -27
  172. data/config/locales/eu.yml +69 -30
  173. data/config/locales/fi.yml +69 -30
  174. data/config/locales/fr.yml +85 -46
  175. data/config/locales/gl.yml +69 -30
  176. data/config/locales/it.yml +69 -30
  177. data/config/locales/nl.yml +122 -83
  178. data/config/locales/pl.yml +69 -30
  179. data/config/locales/pt-BR.yml +69 -30
  180. data/config/locales/pt.yml +69 -30
  181. data/config/locales/ru.yml +0 -7
  182. data/config/locales/sv.yml +69 -30
  183. data/config/locales/uk.yml +0 -13
  184. data/config/routes.rb +8 -0
  185. data/db/migrate/20180206183235_create_participatory_space_private_users.rb +15 -0
  186. data/db/migrate/20180221101934_fix_nickname_index.rb +5 -6
  187. data/db/migrate/20180226140756_add_version_to_action_logs.rb +5 -1
  188. data/db/migrate/20180227131727_create_participatory_space_links.rb +12 -0
  189. data/db/migrate/20180305132906_rename_features_to_components.rb +13 -0
  190. data/db/migrate/20180308113207_doorkeeper_models.rb +85 -0
  191. data/db/migrate/20180314085339_rename_maximum_votes_per_proposal_to_threshold_per_proposal.rb +2 -2
  192. data/db/migrate/{20180326075746_change_event_name_and_class_to_rename_to_publish_proposal_event.rb → 20180323102631_change_event_name_and_class_to_rename_to_publish_proposal_event.rb} +0 -0
  193. data/db/seeds.rb +12 -2
  194. data/lib/decidim/api/attachable_interface.rb +13 -0
  195. data/lib/decidim/api/authorable_interface.rb +13 -0
  196. data/lib/decidim/api/categorizable_interface.rb +13 -0
  197. data/lib/decidim/api/participatory_space_interface.rb +4 -4
  198. data/lib/decidim/api/scopable_interface.rb +13 -0
  199. data/lib/decidim/authorable.rb +8 -0
  200. data/lib/decidim/{feature_manifest.rb → component_manifest.rb} +24 -21
  201. data/lib/decidim/{feature_validator.rb → component_validator.rb} +6 -6
  202. data/lib/decidim/{features → components}/export_manifest.rb +4 -4
  203. data/lib/decidim/components/namer.rb +35 -0
  204. data/lib/decidim/components.rb +9 -0
  205. data/lib/decidim/content_parsers/user_parser.rb +1 -1
  206. data/lib/decidim/core/api.rb +13 -0
  207. data/lib/decidim/core/engine.rb +76 -3
  208. data/lib/decidim/core/test/factories.rb +43 -10
  209. data/lib/decidim/core/test/shared_examples/announcements_examples.rb +9 -9
  210. data/lib/decidim/core/test/shared_examples/attachable_interface_examples.rb +16 -0
  211. data/lib/decidim/core/test/shared_examples/authorable_interface_examples.rb +33 -0
  212. data/lib/decidim/core/test/shared_examples/categorizable_interface_examples.rb +19 -0
  213. data/lib/decidim/core/test/shared_examples/comments_examples.rb +3 -3
  214. data/lib/decidim/core/test/shared_examples/follows_examples.rb +1 -1
  215. data/lib/decidim/core/test/shared_examples/has_component.rb +21 -0
  216. data/lib/decidim/core/test/shared_examples/has_reference.rb +2 -2
  217. data/lib/decidim/core/test/shared_examples/localised_email.rb +1 -1
  218. data/lib/decidim/core/test/shared_examples/paginated_resource_examples.rb +1 -1
  219. data/lib/decidim/core/test/shared_examples/reportable.rb +8 -6
  220. data/lib/decidim/core/test/shared_examples/reports_examples.rb +1 -1
  221. data/lib/decidim/core/test/shared_examples/scopable_interface_examples.rb +19 -0
  222. data/lib/decidim/core/test/shared_examples/scope_helper_examples.rb +8 -3
  223. data/lib/decidim/core/test/shared_examples/simple_event.rb +1 -1
  224. data/lib/decidim/core/test/shared_examples/user_localised_email_examples.rb +1 -1
  225. data/lib/decidim/core/test.rb +1 -1
  226. data/lib/decidim/core/version.rb +1 -1
  227. data/lib/decidim/core.rb +35 -37
  228. data/lib/decidim/events/base_event.rb +5 -5
  229. data/lib/decidim/events/simple_event.rb +8 -8
  230. data/lib/decidim/form_builder.rb +48 -3
  231. data/lib/decidim/has_attachment_collections.rb +1 -1
  232. data/lib/decidim/has_attachments.rb +1 -1
  233. data/lib/decidim/has_category.rb +2 -2
  234. data/lib/decidim/has_component.rb +23 -0
  235. data/lib/decidim/has_private_users.rb +26 -0
  236. data/lib/decidim/has_reference.rb +3 -3
  237. data/lib/decidim/page_finder.rb +1 -1
  238. data/lib/decidim/participatory_space_manifest.rb +3 -3
  239. data/lib/decidim/participatory_space_resourceable.rb +80 -0
  240. data/lib/decidim/publicable.rb +2 -2
  241. data/lib/decidim/query_extensions.rb +2 -2
  242. data/lib/decidim/rectify_ext.rb +32 -0
  243. data/lib/decidim/reportable.rb +1 -1
  244. data/lib/decidim/resource_manifest.rb +13 -13
  245. data/lib/decidim/resourceable.rb +8 -8
  246. data/lib/decidim/scopable.rb +1 -1
  247. data/lib/decidim/{scopable_feature.rb → scopable_component.rb} +1 -1
  248. data/lib/decidim/settings_manifest.rb +1 -1
  249. data/lib/decidim/stats_registry.rb +1 -1
  250. data/lib/decidim/view_model.rb +9 -0
  251. data/vendor/assets/javascripts/datepicker-locales/foundation-datepicker.fr.js +4 -1
  252. data/vendor/assets/javascripts/form_datepicker.js.es6 +10 -10
  253. data/vendor/assets/javascripts/quill.min.js +2 -2
  254. data/vendor/assets/javascripts/quill.min.js.map +1 -1
  255. data/vendor/assets/javascripts/tagsinput.js +683 -0
  256. data/vendor/assets/javascripts/tribute.js +1607 -0
  257. data/vendor/assets/stylesheets/quill.bubble.css +30 -16
  258. data/vendor/assets/stylesheets/quill.core.css +19 -9
  259. data/vendor/assets/stylesheets/quill.snow.css +30 -16
  260. data/vendor/assets/stylesheets/tagsinput.css +55 -0
  261. data/vendor/assets/stylesheets/tribute.css +27 -0
  262. metadata +164 -27
  263. data/app/constraints/decidim/current_feature.rb +0 -41
  264. data/app/helpers/decidim/feature_path_helper.rb +0 -36
  265. data/app/views/layouts/decidim/_feature_authorization_modals.html.erb +0 -5
  266. data/lib/decidim/core/test/shared_examples/has_feature.rb +0 -21
  267. data/lib/decidim/features/namer.rb +0 -35
  268. data/lib/decidim/features.rb +0 -9
  269. data/lib/decidim/has_feature.rb +0 -23
@@ -0,0 +1,683 @@
1
+ (function ($) {
2
+ "use strict";
3
+
4
+ var defaultOptions = {
5
+ tagClass: function(item) {
6
+ return 'label label-info';
7
+ },
8
+ focusClass: 'focus',
9
+ itemValue: function(item) {
10
+ return item ? item.toString() : item;
11
+ },
12
+ itemText: function(item) {
13
+ return this.itemValue(item);
14
+ },
15
+ itemTitle: function(item) {
16
+ return null;
17
+ },
18
+ freeInput: true,
19
+ addOnBlur: true,
20
+ maxTags: undefined,
21
+ maxChars: undefined,
22
+ confirmKeys: [13, 44],
23
+ delimiter: ',',
24
+ delimiterRegex: null,
25
+ cancelConfirmKeysOnEmpty: false,
26
+ onTagExists: function(item, $tag) {
27
+ $tag.hide().fadeIn();
28
+ },
29
+ trimValue: false,
30
+ allowDuplicates: false,
31
+ triggerChange: true
32
+ };
33
+
34
+ /**
35
+ * Constructor function
36
+ */
37
+ function TagsInput(element, options) {
38
+ this.isInit = true;
39
+ this.itemsArray = [];
40
+
41
+ this.$element = $(element);
42
+ this.$element.hide();
43
+
44
+ this.isSelect = (element.tagName === 'SELECT');
45
+ this.multiple = (this.isSelect && element.hasAttribute('multiple'));
46
+ this.objectItems = options && options.itemValue;
47
+ this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : '';
48
+ this.inputSize = Math.max(1, this.placeholderText.length);
49
+
50
+ this.$container = $('<div class="bootstrap-tagsinput"></div>');
51
+ this.$input = $('<input type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container);
52
+
53
+ this.$element.before(this.$container);
54
+
55
+ this.build(options);
56
+ this.isInit = false;
57
+ }
58
+
59
+ TagsInput.prototype = {
60
+ constructor: TagsInput,
61
+
62
+ /**
63
+ * Adds the given item as a new tag. Pass true to dontPushVal to prevent
64
+ * updating the elements val()
65
+ */
66
+ add: function(item, dontPushVal, options) {
67
+ var self = this;
68
+
69
+ if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags)
70
+ return;
71
+
72
+ // Ignore falsey values, except false
73
+ if (item !== false && !item)
74
+ return;
75
+
76
+ // Trim value
77
+ if (typeof item === "string" && self.options.trimValue) {
78
+ item = $.trim(item);
79
+ }
80
+
81
+ // Throw an error when trying to add an object while the itemValue option was not set
82
+ if (typeof item === "object" && !self.objectItems)
83
+ throw("Can't add objects when itemValue option is not set");
84
+
85
+ // Ignore strings only containg whitespace
86
+ if (item.toString().match(/^\s*$/))
87
+ return;
88
+
89
+ // If SELECT but not multiple, remove current tag
90
+ if (self.isSelect && !self.multiple && self.itemsArray.length > 0)
91
+ self.remove(self.itemsArray[0]);
92
+
93
+ if (typeof item === "string" && this.$element[0].tagName === 'INPUT') {
94
+ var delimiter = (self.options.delimiterRegex) ? self.options.delimiterRegex : self.options.delimiter;
95
+ var items = item.split(delimiter);
96
+ if (items.length > 1) {
97
+ for (var i = 0; i < items.length; i++) {
98
+ this.add(items[i], true);
99
+ }
100
+
101
+ if (!dontPushVal)
102
+ self.pushVal(self.options.triggerChange);
103
+ return;
104
+ }
105
+ }
106
+
107
+ var itemValue = self.options.itemValue(item),
108
+ itemText = self.options.itemText(item),
109
+ tagClass = self.options.tagClass(item),
110
+ itemTitle = self.options.itemTitle(item);
111
+
112
+ // Ignore items allready added
113
+ var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0];
114
+ if (existing && !self.options.allowDuplicates) {
115
+ // Invoke onTagExists
116
+ if (self.options.onTagExists) {
117
+ var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; });
118
+ self.options.onTagExists(item, $existingTag);
119
+ }
120
+ return;
121
+ }
122
+
123
+ // if length greater than limit
124
+ if (self.items().toString().length + item.length + 1 > self.options.maxInputLength)
125
+ return;
126
+
127
+ // raise beforeItemAdd arg
128
+ var beforeItemAddEvent = $.Event('beforeItemAdd', { item: item, cancel: false, options: options});
129
+ self.$element.trigger(beforeItemAddEvent);
130
+ if (beforeItemAddEvent.cancel)
131
+ return;
132
+
133
+ // register item in internal array and map
134
+ self.itemsArray.push(item);
135
+
136
+ // add a tag element
137
+
138
+ var $tag = $('<span class="tag ' + htmlEncode(tagClass) + (itemTitle !== null ? ('" title="' + itemTitle) : '') + '">' + htmlEncode(itemText) + '<span data-role="remove"></span></span>');
139
+ $tag.data('item', item);
140
+ self.findInputWrapper().before($tag);
141
+ $tag.after(' ');
142
+
143
+ // Check to see if the tag exists in its raw or uri-encoded form
144
+ var optionExists = (
145
+ $('option[value="' + encodeURIComponent(itemValue) + '"]', self.$element).length ||
146
+ $('option[value="' + htmlEncode(itemValue) + '"]', self.$element).length
147
+ );
148
+
149
+ // add <option /> if item represents a value not present in one of the <select />'s options
150
+ if (self.isSelect && !optionExists) {
151
+ var $option = $('<option selected>' + htmlEncode(itemText) + '</option>');
152
+ $option.data('item', item);
153
+ $option.attr('value', itemValue);
154
+ self.$element.append($option);
155
+ }
156
+
157
+ if (!dontPushVal)
158
+ self.pushVal(self.options.triggerChange);
159
+
160
+ // Add class when reached maxTags
161
+ if (self.options.maxTags === self.itemsArray.length || self.items().toString().length === self.options.maxInputLength)
162
+ self.$container.addClass('bootstrap-tagsinput-max');
163
+
164
+ // If using typeahead, once the tag has been added, clear the typeahead value so it does not stick around in the input.
165
+ if ($('.typeahead, .twitter-typeahead', self.$container).length) {
166
+ self.$input.typeahead('val', '');
167
+ }
168
+
169
+ if (this.isInit) {
170
+ self.$element.trigger($.Event('itemAddedOnInit', { item: item, options: options }));
171
+ } else {
172
+ self.$element.trigger($.Event('itemAdded', { item: item, options: options }));
173
+ }
174
+ },
175
+
176
+ /**
177
+ * Removes the given item. Pass true to dontPushVal to prevent updating the
178
+ * elements val()
179
+ */
180
+ remove: function(item, dontPushVal, options) {
181
+ var self = this;
182
+
183
+ if (self.objectItems) {
184
+ if (typeof item === "object")
185
+ item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } );
186
+ else
187
+ item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } );
188
+
189
+ item = item[item.length-1];
190
+ }
191
+
192
+ if (item) {
193
+ var beforeItemRemoveEvent = $.Event('beforeItemRemove', { item: item, cancel: false, options: options });
194
+ self.$element.trigger(beforeItemRemoveEvent);
195
+ if (beforeItemRemoveEvent.cancel)
196
+ return;
197
+
198
+ $('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove();
199
+ $('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove();
200
+ if($.inArray(item, self.itemsArray) !== -1)
201
+ self.itemsArray.splice($.inArray(item, self.itemsArray), 1);
202
+ }
203
+
204
+ if (!dontPushVal)
205
+ self.pushVal(self.options.triggerChange);
206
+
207
+ // Remove class when reached maxTags
208
+ if (self.options.maxTags > self.itemsArray.length)
209
+ self.$container.removeClass('bootstrap-tagsinput-max');
210
+
211
+ self.$element.trigger($.Event('itemRemoved', { item: item, options: options }));
212
+ },
213
+
214
+ /**
215
+ * Removes all items
216
+ */
217
+ removeAll: function() {
218
+ var self = this;
219
+
220
+ $('.tag', self.$container).remove();
221
+ $('option', self.$element).remove();
222
+
223
+ while(self.itemsArray.length > 0)
224
+ self.itemsArray.pop();
225
+
226
+ self.pushVal(self.options.triggerChange);
227
+ },
228
+
229
+ /**
230
+ * Refreshes the tags so they match the text/value of their corresponding
231
+ * item.
232
+ */
233
+ refresh: function() {
234
+ var self = this;
235
+ $('.tag', self.$container).each(function() {
236
+ var $tag = $(this),
237
+ item = $tag.data('item'),
238
+ itemValue = self.options.itemValue(item),
239
+ itemText = self.options.itemText(item),
240
+ tagClass = self.options.tagClass(item);
241
+
242
+ // Update tag's class and inner text
243
+ $tag.attr('class', null);
244
+ $tag.addClass('tag ' + htmlEncode(tagClass));
245
+ $tag.contents().filter(function() {
246
+ return this.nodeType == 3;
247
+ })[0].nodeValue = htmlEncode(itemText);
248
+
249
+ if (self.isSelect) {
250
+ var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; });
251
+ option.attr('value', itemValue);
252
+ }
253
+ });
254
+ },
255
+
256
+ /**
257
+ * Returns the items added as tags
258
+ */
259
+ items: function() {
260
+ return this.itemsArray;
261
+ },
262
+
263
+ /**
264
+ * Assembly value by retrieving the value of each item, and set it on the
265
+ * element.
266
+ */
267
+ pushVal: function() {
268
+ var self = this,
269
+ val = $.map(self.items(), function(item) {
270
+ return self.options.itemValue(item).toString();
271
+ });
272
+
273
+ self.$element.val(val, true);
274
+
275
+ if (self.options.triggerChange)
276
+ self.$element.trigger('change');
277
+ },
278
+
279
+ /**
280
+ * Initializes the tags input behaviour on the element
281
+ */
282
+ build: function(options) {
283
+ var self = this;
284
+
285
+ self.options = $.extend({}, defaultOptions, options);
286
+ // When itemValue is set, freeInput should always be false
287
+ if (self.objectItems)
288
+ self.options.freeInput = false;
289
+
290
+ makeOptionItemFunction(self.options, 'itemValue');
291
+ makeOptionItemFunction(self.options, 'itemText');
292
+ makeOptionFunction(self.options, 'tagClass');
293
+
294
+ // Typeahead Bootstrap version 2.3.2
295
+ if (self.options.typeahead) {
296
+ var typeahead = self.options.typeahead || {};
297
+
298
+ makeOptionFunction(typeahead, 'source');
299
+
300
+ self.$input.typeahead($.extend({}, typeahead, {
301
+ source: function (query, process) {
302
+ function processItems(items) {
303
+ var texts = [];
304
+
305
+ for (var i = 0; i < items.length; i++) {
306
+ var text = self.options.itemText(items[i]);
307
+ map[text] = items[i];
308
+ texts.push(text);
309
+ }
310
+ process(texts);
311
+ }
312
+
313
+ this.map = {};
314
+ var map = this.map,
315
+ data = typeahead.source(query);
316
+
317
+ if ($.isFunction(data.success)) {
318
+ // support for Angular callbacks
319
+ data.success(processItems);
320
+ } else if ($.isFunction(data.then)) {
321
+ // support for Angular promises
322
+ data.then(processItems);
323
+ } else {
324
+ // support for functions and jquery promises
325
+ $.when(data)
326
+ .then(processItems);
327
+ }
328
+ },
329
+ updater: function (text) {
330
+ self.add(this.map[text]);
331
+ return this.map[text];
332
+ },
333
+ matcher: function (text) {
334
+ return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1);
335
+ },
336
+ sorter: function (texts) {
337
+ return texts.sort();
338
+ },
339
+ highlighter: function (text) {
340
+ var regex = new RegExp( '(' + this.query + ')', 'gi' );
341
+ return text.replace( regex, "<strong>$1</strong>" );
342
+ }
343
+ }));
344
+ }
345
+
346
+ // typeahead.js
347
+ if (self.options.typeaheadjs) {
348
+ // Determine if main configurations were passed or simply a dataset
349
+ var typeaheadjs = self.options.typeaheadjs;
350
+ if (!$.isArray(typeaheadjs)) {
351
+ typeaheadjs = [null, typeaheadjs];
352
+ }
353
+
354
+ $.fn.typeahead.apply(self.$input, typeaheadjs).on('typeahead:selected', $.proxy(function (obj, datum, name) {
355
+ var index = 0;
356
+ typeaheadjs.some(function(dataset, _index) {
357
+ if (dataset.name === name) {
358
+ index = _index;
359
+ return true;
360
+ }
361
+ return false;
362
+ });
363
+
364
+ // @TODO Dep: https://github.com/corejavascript/typeahead.js/issues/89
365
+ if (typeaheadjs[index].valueKey) {
366
+ self.add(datum[typeaheadjs[index].valueKey]);
367
+ } else {
368
+ self.add(datum);
369
+ }
370
+
371
+ self.$input.typeahead('val', '');
372
+ }, self));
373
+ }
374
+
375
+ self.$container.on('click', $.proxy(function(event) {
376
+ if (! self.$element.attr('disabled')) {
377
+ self.$input.removeAttr('disabled');
378
+ }
379
+ self.$input.focus();
380
+ }, self));
381
+
382
+ if (self.options.addOnBlur && self.options.freeInput) {
383
+ self.$input.on('focusout', $.proxy(function(event) {
384
+ // HACK: only process on focusout when no typeahead opened, to
385
+ // avoid adding the typeahead text as tag
386
+ if ($('.typeahead, .twitter-typeahead', self.$container).length === 0) {
387
+ self.add(self.$input.val());
388
+ self.$input.val('');
389
+ }
390
+ }, self));
391
+ }
392
+
393
+ // Toggle the 'focus' css class on the container when it has focus
394
+ self.$container.on({
395
+ focusin: function() {
396
+ self.$container.addClass(self.options.focusClass);
397
+ },
398
+ focusout: function() {
399
+ self.$container.removeClass(self.options.focusClass);
400
+ },
401
+ });
402
+
403
+ self.$container.on('keydown', 'input', $.proxy(function(event) {
404
+ var $input = $(event.target),
405
+ $inputWrapper = self.findInputWrapper();
406
+
407
+ if (self.$element.attr('disabled')) {
408
+ self.$input.attr('disabled', 'disabled');
409
+ return;
410
+ }
411
+
412
+ switch (event.which) {
413
+ // BACKSPACE
414
+ case 8:
415
+ if (doGetCaretPosition($input[0]) === 0) {
416
+ var prev = $inputWrapper.prev();
417
+ if (prev.length) {
418
+ self.remove(prev.data('item'));
419
+ }
420
+ }
421
+ break;
422
+
423
+ // DELETE
424
+ case 46:
425
+ if (doGetCaretPosition($input[0]) === 0) {
426
+ var next = $inputWrapper.next();
427
+ if (next.length) {
428
+ self.remove(next.data('item'));
429
+ }
430
+ }
431
+ break;
432
+
433
+ // LEFT ARROW
434
+ case 37:
435
+ // Try to move the input before the previous tag
436
+ var $prevTag = $inputWrapper.prev();
437
+ if ($input.val().length === 0 && $prevTag[0]) {
438
+ $prevTag.before($inputWrapper);
439
+ $input.focus();
440
+ }
441
+ break;
442
+ // RIGHT ARROW
443
+ case 39:
444
+ // Try to move the input after the next tag
445
+ var $nextTag = $inputWrapper.next();
446
+ if ($input.val().length === 0 && $nextTag[0]) {
447
+ $nextTag.after($inputWrapper);
448
+ $input.focus();
449
+ }
450
+ break;
451
+ default:
452
+ // ignore
453
+ }
454
+
455
+ // Reset internal input's size
456
+ var textLength = $input.val().length,
457
+ wordSpace = Math.ceil(textLength / 5),
458
+ size = textLength + wordSpace + 1;
459
+ $input.attr('size', Math.max(this.inputSize, $input.val().length));
460
+ }, self));
461
+
462
+ self.$container.on('keypress', 'input', $.proxy(function(event) {
463
+ var $input = $(event.target);
464
+
465
+ if (self.$element.attr('disabled')) {
466
+ self.$input.attr('disabled', 'disabled');
467
+ return;
468
+ }
469
+
470
+ var text = $input.val(),
471
+ maxLengthReached = self.options.maxChars && text.length >= self.options.maxChars;
472
+ if (self.options.freeInput && (keyCombinationInList(event, self.options.confirmKeys) || maxLengthReached)) {
473
+ // Only attempt to add a tag if there is data in the field
474
+ if (text.length !== 0) {
475
+ self.add(maxLengthReached ? text.substr(0, self.options.maxChars) : text);
476
+ $input.val('');
477
+ }
478
+
479
+ // If the field is empty, let the event triggered fire as usual
480
+ if (self.options.cancelConfirmKeysOnEmpty === false) {
481
+ event.preventDefault();
482
+ }
483
+ }
484
+
485
+ // Reset internal input's size
486
+ var textLength = $input.val().length,
487
+ wordSpace = Math.ceil(textLength / 5),
488
+ size = textLength + wordSpace + 1;
489
+ $input.attr('size', Math.max(this.inputSize, $input.val().length));
490
+ }, self));
491
+
492
+ // Remove icon clicked
493
+ self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
494
+ if (self.$element.attr('disabled')) {
495
+ return;
496
+ }
497
+ self.remove($(event.target).closest('.tag').data('item'));
498
+ }, self));
499
+
500
+ // Only add existing value as tags when using strings as tags
501
+ if (self.options.itemValue === defaultOptions.itemValue) {
502
+ if (self.$element[0].tagName === 'INPUT') {
503
+ self.add(self.$element.val());
504
+ } else {
505
+ $('option', self.$element).each(function() {
506
+ self.add($(this).attr('value'), true);
507
+ });
508
+ }
509
+ }
510
+ },
511
+
512
+ /**
513
+ * Removes all tagsinput behaviour and unregsiter all event handlers
514
+ */
515
+ destroy: function() {
516
+ var self = this;
517
+
518
+ // Unbind events
519
+ self.$container.off('keypress', 'input');
520
+ self.$container.off('click', '[role=remove]');
521
+
522
+ self.$container.remove();
523
+ self.$element.removeData('tagsinput');
524
+ self.$element.show();
525
+ },
526
+
527
+ /**
528
+ * Sets focus on the tagsinput
529
+ */
530
+ focus: function() {
531
+ this.$input.focus();
532
+ },
533
+
534
+ /**
535
+ * Returns the internal input element
536
+ */
537
+ input: function() {
538
+ return this.$input;
539
+ },
540
+
541
+ /**
542
+ * Returns the element which is wrapped around the internal input. This
543
+ * is normally the $container, but typeahead.js moves the $input element.
544
+ */
545
+ findInputWrapper: function() {
546
+ var elt = this.$input[0],
547
+ container = this.$container[0];
548
+ while(elt && elt.parentNode !== container)
549
+ elt = elt.parentNode;
550
+
551
+ return $(elt);
552
+ }
553
+ };
554
+
555
+ /**
556
+ * Register JQuery plugin
557
+ */
558
+ $.fn.tagsinput = function(arg1, arg2, arg3) {
559
+ var results = [];
560
+
561
+ this.each(function() {
562
+ var tagsinput = $(this).data('tagsinput');
563
+ // Initialize a new tags input
564
+ if (!tagsinput) {
565
+ tagsinput = new TagsInput(this, arg1);
566
+ $(this).data('tagsinput', tagsinput);
567
+ results.push(tagsinput);
568
+
569
+ if (this.tagName === 'SELECT') {
570
+ $('option', $(this)).attr('selected', 'selected');
571
+ }
572
+
573
+ // Init tags from $(this).val()
574
+ $(this).val($(this).val());
575
+ } else if (!arg1 && !arg2) {
576
+ // tagsinput already exists
577
+ // no function, trying to init
578
+ results.push(tagsinput);
579
+ } else if(tagsinput[arg1] !== undefined) {
580
+ // Invoke function on existing tags input
581
+ if(tagsinput[arg1].length === 3 && arg3 !== undefined){
582
+ var retVal = tagsinput[arg1](arg2, null, arg3);
583
+ }else{
584
+ var retVal = tagsinput[arg1](arg2);
585
+ }
586
+ if (retVal !== undefined)
587
+ results.push(retVal);
588
+ }
589
+ });
590
+
591
+ if ( typeof arg1 == 'string') {
592
+ // Return the results from the invoked function calls
593
+ return results.length > 1 ? results : results[0];
594
+ } else {
595
+ return results;
596
+ }
597
+ };
598
+
599
+ $.fn.tagsinput.Constructor = TagsInput;
600
+
601
+ /**
602
+ * Most options support both a string or number as well as a function as
603
+ * option value. This function makes sure that the option with the given
604
+ * key in the given options is wrapped in a function
605
+ */
606
+ function makeOptionItemFunction(options, key) {
607
+ if (typeof options[key] !== 'function') {
608
+ var propertyName = options[key];
609
+ options[key] = function(item) { return item[propertyName]; };
610
+ }
611
+ }
612
+ function makeOptionFunction(options, key) {
613
+ if (typeof options[key] !== 'function') {
614
+ var value = options[key];
615
+ options[key] = function() { return value; };
616
+ }
617
+ }
618
+ /**
619
+ * HtmlEncodes the given value
620
+ */
621
+ var htmlEncodeContainer = $('<div />');
622
+ function htmlEncode(value) {
623
+ if (value) {
624
+ return htmlEncodeContainer.text(value).html();
625
+ } else {
626
+ return '';
627
+ }
628
+ }
629
+
630
+ /**
631
+ * Returns the position of the caret in the given input field
632
+ * http://flightschool.acylt.com/devnotes/caret-position-woes/
633
+ */
634
+ function doGetCaretPosition(oField) {
635
+ var iCaretPos = 0;
636
+ if (document.selection) {
637
+ oField.focus ();
638
+ var oSel = document.selection.createRange();
639
+ oSel.moveStart ('character', -oField.value.length);
640
+ iCaretPos = oSel.text.length;
641
+ } else if (oField.selectionStart || oField.selectionStart == '0') {
642
+ iCaretPos = oField.selectionStart;
643
+ }
644
+ return (iCaretPos);
645
+ }
646
+
647
+ /**
648
+ * Returns boolean indicates whether user has pressed an expected key combination.
649
+ * @param object keyPressEvent: JavaScript event object, refer
650
+ * http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
651
+ * @param object lookupList: expected key combinations, as in:
652
+ * [13, {which: 188, shiftKey: true}]
653
+ */
654
+ function keyCombinationInList(keyPressEvent, lookupList) {
655
+ var found = false;
656
+ $.each(lookupList, function (index, keyCombination) {
657
+ if (typeof (keyCombination) === 'number' && keyPressEvent.which === keyCombination) {
658
+ found = true;
659
+ return false;
660
+ }
661
+
662
+ if (keyPressEvent.which === keyCombination.which) {
663
+ var alt = !keyCombination.hasOwnProperty('altKey') || keyPressEvent.altKey === keyCombination.altKey,
664
+ shift = !keyCombination.hasOwnProperty('shiftKey') || keyPressEvent.shiftKey === keyCombination.shiftKey,
665
+ ctrl = !keyCombination.hasOwnProperty('ctrlKey') || keyPressEvent.ctrlKey === keyCombination.ctrlKey;
666
+ if (alt && shift && ctrl) {
667
+ found = true;
668
+ return false;
669
+ }
670
+ }
671
+ });
672
+
673
+ return found;
674
+ }
675
+
676
+ /**
677
+ * Initialize tagsinput behaviour on inputs and selects which have
678
+ * data-role=tagsinput
679
+ */
680
+ $(function() {
681
+ $("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput();
682
+ });
683
+ })(window.jQuery);