decidim-admin 0.20.1 → 0.21.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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -0
  3. data/app/assets/javascripts/decidim/admin/application.js.es6 +1 -0
  4. data/app/assets/javascripts/decidim/admin/bundle.js +5 -5
  5. data/app/assets/javascripts/decidim/admin/bundle.js.map +1 -1
  6. data/app/assets/javascripts/decidim/admin/newsletters.js.es6 +8 -0
  7. data/app/assets/stylesheets/decidim/admin/_variables.scss +1 -1
  8. data/app/assets/stylesheets/decidim/admin/extra/_action-icon.scss +6 -0
  9. data/app/assets/stylesheets/decidim/admin/extra/_cards.scss +11 -0
  10. data/app/assets/stylesheets/decidim/admin/modules/_buttons.scss +5 -0
  11. data/app/assets/stylesheets/decidim/admin/modules/_cards.scss +11 -0
  12. data/app/assets/stylesheets/decidim/admin/modules/_filters.scss +78 -1
  13. data/app/assets/stylesheets/decidim/admin/modules/_layout.scss +1 -1
  14. data/app/assets/stylesheets/decidim/admin/modules/_secondary-nav.scss +5 -1
  15. data/app/assets/stylesheets/decidim/admin/modules/_table-list.scss +24 -3
  16. data/app/cells/decidim/admin/results_per_page/show.erb +16 -0
  17. data/app/cells/decidim/admin/results_per_page_cell.rb +14 -0
  18. data/app/commands/decidim/admin/deliver_newsletter.rb +1 -1
  19. data/app/commands/decidim/admin/update_organization.rb +4 -1
  20. data/app/controllers/concerns/decidim/admin/filterable.rb +152 -0
  21. data/app/controllers/concerns/decidim/admin/officializations/filterable.rb +31 -0
  22. data/app/controllers/concerns/decidim/admin/paginable.rb +20 -0
  23. data/app/controllers/decidim/admin/admin_terms_controller.rb +20 -0
  24. data/app/controllers/decidim/admin/application_controller.rb +2 -0
  25. data/app/controllers/decidim/admin/components/base_controller.rb +5 -1
  26. data/app/controllers/decidim/admin/components_controller.rb +16 -20
  27. data/app/controllers/decidim/admin/concerns/has_private_users.rb +4 -0
  28. data/app/controllers/decidim/admin/newsletters_controller.rb +12 -1
  29. data/app/controllers/decidim/admin/officializations_controller.rb +7 -6
  30. data/app/forms/decidim/admin/organization_form.rb +7 -0
  31. data/app/helpers/decidim/admin/admin_terms_helper.rb +47 -0
  32. data/app/helpers/decidim/admin/application_helper.rb +1 -0
  33. data/app/helpers/decidim/admin/dashboard_helper.rb +25 -0
  34. data/app/helpers/decidim/admin/filterable_helper.rb +121 -0
  35. data/app/helpers/decidim/admin/newsletters_helper.rb +18 -0
  36. data/app/helpers/decidim/admin/paginable/per_page_helper.rb +22 -0
  37. data/app/helpers/decidim/admin/scopes_helper.rb +6 -0
  38. data/app/helpers/decidim/admin/settings_helper.rb +18 -2
  39. data/app/helpers/decidim/admin/user_roles_helper.rb +19 -0
  40. data/app/permissions/decidim/admin/permissions.rb +23 -6
  41. data/app/queries/decidim/admin/newsletter_recipients.rb +11 -4
  42. data/app/views/decidim/admin/admin_terms/show.html.erb +26 -0
  43. data/app/views/decidim/admin/components/_component.html.erb +35 -33
  44. data/app/views/decidim/admin/components/index.html.erb +10 -8
  45. data/app/views/decidim/admin/dashboard/show.html.erb +15 -0
  46. data/app/views/decidim/admin/newsletters/index.html.erb +9 -3
  47. data/app/views/decidim/admin/newsletters/select_recipients_to_deliver.html.erb +4 -4
  48. data/app/views/decidim/admin/officializations/index.html.erb +2 -38
  49. data/app/views/decidim/admin/organization/_form.html.erb +16 -0
  50. data/app/views/decidim/admin/shared/_filters.html.erb +40 -0
  51. data/app/views/layouts/decidim/admin/_application.html.erb +1 -1
  52. data/config/locales/ar.yml +40 -6
  53. data/config/locales/ca.yml +45 -6
  54. data/config/locales/cs.yml +45 -6
  55. data/config/locales/de.yml +9 -6
  56. data/config/locales/el.yml +1 -0
  57. data/config/locales/en.yml +45 -6
  58. data/config/locales/es-MX.yml +45 -6
  59. data/config/locales/es-PY.yml +45 -6
  60. data/config/locales/es.yml +45 -6
  61. data/config/locales/eu.yml +9 -6
  62. data/config/locales/fi-plain.yml +45 -6
  63. data/config/locales/fi.yml +45 -6
  64. data/config/locales/fr.yml +9 -6
  65. data/config/locales/gl.yml +9 -6
  66. data/config/locales/hu.yml +45 -6
  67. data/config/locales/id-ID.yml +9 -6
  68. data/config/locales/is-IS.yml +9 -6
  69. data/config/locales/it.yml +45 -6
  70. data/config/locales/nl.yml +28 -6
  71. data/config/locales/no.yml +45 -6
  72. data/config/locales/pl.yml +9 -6
  73. data/config/locales/pt-BR.yml +9 -6
  74. data/config/locales/pt.yml +9 -6
  75. data/config/locales/ru.yml +9 -6
  76. data/config/locales/sv.yml +9 -6
  77. data/config/locales/tr-TR.yml +9 -6
  78. data/config/locales/uk.yml +9 -6
  79. data/config/routes.rb +6 -0
  80. data/db/migrate/20191118112040_add_accepted_admin_terms_at_field_to_users.rb +7 -0
  81. data/lib/decidim/admin.rb +17 -0
  82. data/lib/decidim/admin/form_builder.rb +5 -0
  83. data/lib/decidim/admin/test.rb +2 -0
  84. data/lib/decidim/admin/test/filterable_examples.rb +129 -0
  85. data/lib/decidim/admin/test/manage_paginated_collection_examples.rb +22 -0
  86. data/lib/decidim/admin/version.rb +1 -1
  87. data/vendor/assets/javascripts/jquery.serializejson.js +344 -0
  88. metadata +26 -8
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddAcceptedAdminTermsAtFieldToUsers < ActiveRecord::Migration[5.2]
4
+ def change
5
+ add_column :decidim_users, :admin_terms_accepted_at, :datetime
6
+ end
7
+ end
@@ -11,5 +11,22 @@ module Decidim
11
11
  module Admin
12
12
  autoload :Components, "decidim/admin/components"
13
13
  autoload :FormBuilder, "decidim/admin/form_builder"
14
+
15
+ include ActiveSupport::Configurable
16
+
17
+ # Public Setting that configures Kaminari configuration options
18
+ # https://github.com/kaminari/kaminari#general-configuration-options
19
+
20
+ # Range of number of results per_page. Defaults to [15, 50, 100].
21
+ # per_page_range.first sets the default number per page
22
+ # per_page_range.last sets the default max_per_page
23
+ config_accessor :per_page_range do
24
+ [15, 50, 100]
25
+ end
26
+
27
+ Kaminari.configure do |config|
28
+ config.default_per_page = Decidim::Admin.per_page_range.first
29
+ config.max_per_page = Decidim::Admin.per_page_range.last
30
+ end
14
31
  end
15
32
  end
@@ -60,6 +60,11 @@ module Decidim
60
60
  template += error_for(attribute, options) if error?(attribute)
61
61
  template.html_safe
62
62
  end
63
+
64
+ # Calls Decidim::FormBuilder#editor with default options for admin.
65
+ def editor(name, options = {})
66
+ super(name, { toolbar: :full, lines: 25 }.merge(options))
67
+ end
63
68
  end
64
69
  end
65
70
  end
@@ -5,3 +5,5 @@ require "decidim/admin/test/manage_attachment_collections_examples"
5
5
  require "decidim/admin/test/manage_categories_examples"
6
6
  require "decidim/admin/test/manage_component_permissions_examples"
7
7
  require "decidim/admin/test/manage_moderations_examples"
8
+ require "decidim/admin/test/manage_paginated_collection_examples"
9
+ require "decidim/admin/test/filterable_examples"
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_context "with filterable context" do
4
+ let(:factory_name) { model_name.singular_route_key }
5
+ let(:module_name) { model_name.route_key.camelize }
6
+ let(:filterable_concern) { "Decidim::#{module_name}::Admin::Filterable".constantize }
7
+
8
+ let(:filterable_fake_controller) do
9
+ FILTERABLE_CONCERN ||= filterable_concern
10
+ class FilterableFakeController < Decidim::ApplicationController; include FILTERABLE_CONCERN; end
11
+ end
12
+
13
+ def filterable_method(method_name)
14
+ (@controller ||= filterable_fake_controller.new).send(method_name)
15
+ end
16
+
17
+ def apply_filter(options, filter)
18
+ within(".filters__section") do
19
+ find_link("Filter").hover
20
+ find_link(options).hover
21
+ click_link(filter, href: /q/)
22
+ end
23
+ end
24
+
25
+ def remove_applied_filter(filter)
26
+ within(".label", text: /#{filter}/i) do
27
+ click_link("Cancel")
28
+ end
29
+ end
30
+
31
+ def search_by_text(text)
32
+ within(".filters__section") do
33
+ fill_in("q[#{filterable_method(:search_field_predicate)}]", with: text)
34
+ find("*[type=submit]").click
35
+ end
36
+ end
37
+
38
+ shared_examples "paginating a collection" do
39
+ unless block_given?
40
+ let!(:collection) do
41
+ create_list(factory_name, 50, organization: organization)
42
+ end
43
+ end
44
+
45
+ it_behaves_like "a paginated collection"
46
+ end
47
+
48
+ shared_examples "searching by text" do
49
+ before { search_by_text(text) }
50
+
51
+ it { expect(page).to have_content(text) }
52
+
53
+ after { search_by_text("") }
54
+ end
55
+
56
+ shared_examples "a filtered collection" do |options:, filter:|
57
+ before { apply_filter(options, filter) }
58
+
59
+ it { expect(page).to have_content(in_filter) }
60
+ it { expect(page).not_to have_content(not_in_filter) }
61
+
62
+ it_behaves_like "searching by text" do
63
+ let(:text) { in_filter }
64
+ end
65
+
66
+ context "when removing applied filter" do
67
+ before { remove_applied_filter(filter) }
68
+
69
+ it { expect(page).to have_content(in_filter) }
70
+ it { expect(page).to have_content(not_in_filter) }
71
+
72
+ it_behaves_like "searching by text" do
73
+ let(:text) { not_in_filter }
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ shared_examples "filtering collection by published/unpublished" do
80
+ include_context "with filterable context"
81
+
82
+ unless block_given?
83
+ let!(:published_space) do
84
+ create(factory_name, published_at: Time.current, organization: organization)
85
+ end
86
+
87
+ let!(:unpublished_space) do
88
+ create(factory_name, published_at: nil, organization: organization)
89
+ end
90
+ end
91
+
92
+ it_behaves_like "a filtered collection", options: "Published", filter: "Published" do
93
+ let(:in_filter) { translated(published_space.title) }
94
+ let(:not_in_filter) { translated(unpublished_space.title) }
95
+ end
96
+
97
+ it_behaves_like "a filtered collection", options: "Published", filter: "Unpublished" do
98
+ let(:in_filter) { translated(unpublished_space.title) }
99
+ let(:not_in_filter) { translated(published_space.title) }
100
+ end
101
+
102
+ it_behaves_like "paginating a collection"
103
+ end
104
+
105
+ shared_examples "filtering collection by private/public" do
106
+ include_context "with filterable context"
107
+
108
+ unless block_given?
109
+ let!(:public_space) do
110
+ create(factory_name, private_space: false, organization: organization)
111
+ end
112
+
113
+ let!(:private_space) do
114
+ create(factory_name, private_space: true, organization: organization)
115
+ end
116
+ end
117
+
118
+ it_behaves_like "a filtered collection", options: "Private", filter: "Public" do
119
+ let(:in_filter) { translated(public_space.title) }
120
+ let(:not_in_filter) { translated(private_space.title) }
121
+ end
122
+
123
+ it_behaves_like "a filtered collection", options: "Private", filter: "Private" do
124
+ let(:in_filter) { translated(private_space.title) }
125
+ let(:not_in_filter) { translated(public_space.title) }
126
+ end
127
+
128
+ it_behaves_like "paginating a collection"
129
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_examples "a paginated collection" do
4
+ before do
5
+ visit current_path
6
+ end
7
+
8
+ describe "Number of results per page" do
9
+ it "lists 15 resources per page by default" do
10
+ expect(page).to have_css(".table-list tbody tr", count: 15)
11
+ end
12
+
13
+ it "changes the number of results per page" do
14
+ within ".results-per-page__dropdown" do
15
+ page.find("a", text: "15").click
16
+ click_link "50"
17
+ end
18
+
19
+ expect(page).to have_selector(".table-list tbody tr", count: 50)
20
+ end
21
+ end
22
+ end
@@ -4,7 +4,7 @@ module Decidim
4
4
  # This holds the decidim-admin version.
5
5
  module Admin
6
6
  def self.version
7
- "0.20.1"
7
+ "0.21.0"
8
8
  end
9
9
  end
10
10
  end
@@ -0,0 +1,344 @@
1
+ /*!
2
+ SerializeJSON jQuery plugin.
3
+ https://github.com/marioizquierdo/jquery.serializeJSON
4
+ version 2.9.0 (Jan, 2018)
5
+
6
+ Copyright (c) 2012-2018 Mario Izquierdo
7
+ Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
8
+ and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
9
+ */
10
+ (function (factory) {
11
+ if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module.
12
+ define(['jquery'], factory);
13
+ } else if (typeof exports === 'object') { // Node/CommonJS
14
+ var jQuery = require('jquery');
15
+ module.exports = factory(jQuery);
16
+ } else { // Browser globals (zepto supported)
17
+ factory(window.jQuery || window.Zepto || window.$); // Zepto supported on browsers as well
18
+ }
19
+
20
+ }(function ($) {
21
+ "use strict";
22
+
23
+ // jQuery('form').serializeJSON()
24
+ $.fn.serializeJSON = function (options) {
25
+ var f, $form, opts, formAsArray, serializedObject, name, value, parsedValue, _obj, nameWithNoType, type, keys, skipFalsy;
26
+ f = $.serializeJSON;
27
+ $form = this; // NOTE: the set of matched elements is most likely a form, but it could also be a group of inputs
28
+ opts = f.setupOpts(options); // calculate values for options {parseNumbers, parseBoolens, parseNulls, ...} with defaults
29
+
30
+ // Use native `serializeArray` function to get an array of {name, value} objects.
31
+ formAsArray = $form.serializeArray();
32
+ f.readCheckboxUncheckedValues(formAsArray, opts, $form); // add objects to the array from unchecked checkboxes if needed
33
+
34
+ // Convert the formAsArray into a serializedObject with nested keys
35
+ serializedObject = {};
36
+ $.each(formAsArray, function (i, obj) {
37
+ name = obj.name; // original input name
38
+ value = obj.value; // input value
39
+ _obj = f.extractTypeAndNameWithNoType(name);
40
+ nameWithNoType = _obj.nameWithNoType; // input name with no type (i.e. "foo:string" => "foo")
41
+ type = _obj.type; // type defined from the input name in :type colon notation
42
+ if (!type) type = f.attrFromInputWithName($form, name, 'data-value-type');
43
+ f.validateType(name, type, opts); // make sure that the type is one of the valid types if defined
44
+
45
+ if (type !== 'skip') { // ignore inputs with type 'skip'
46
+ keys = f.splitInputNameIntoKeysArray(nameWithNoType);
47
+ parsedValue = f.parseValue(value, name, type, opts); // convert to string, number, boolean, null or customType
48
+
49
+ skipFalsy = !parsedValue && f.shouldSkipFalsy($form, name, nameWithNoType, type, opts); // ignore falsy inputs if specified
50
+ if (!skipFalsy) {
51
+ f.deepSet(serializedObject, keys, parsedValue, opts);
52
+ }
53
+ }
54
+ });
55
+ return serializedObject;
56
+ };
57
+
58
+ // Use $.serializeJSON as namespace for the auxiliar functions
59
+ // and to define defaults
60
+ $.serializeJSON = {
61
+
62
+ defaultOptions: {
63
+ checkboxUncheckedValue: undefined, // to include that value for unchecked checkboxes (instead of ignoring them)
64
+
65
+ parseNumbers: false, // convert values like "1", "-2.33" to 1, -2.33
66
+ parseBooleans: false, // convert "true", "false" to true, false
67
+ parseNulls: false, // convert "null" to null
68
+ parseAll: false, // all of the above
69
+ parseWithFunction: null, // to use custom parser, a function like: function(val){ return parsed_val; }
70
+
71
+ skipFalsyValuesForTypes: [], // skip serialization of falsy values for listed value types
72
+ skipFalsyValuesForFields: [], // skip serialization of falsy values for listed field names
73
+
74
+ customTypes: {}, // override defaultTypes
75
+ defaultTypes: {
76
+ "string": function(str) { return String(str); },
77
+ "number": function(str) { return Number(str); },
78
+ "boolean": function(str) { var falses = ["false", "null", "undefined", "", "0"]; return falses.indexOf(str) === -1; },
79
+ "null": function(str) { var falses = ["false", "null", "undefined", "", "0"]; return falses.indexOf(str) === -1 ? str : null; },
80
+ "array": function(str) { return JSON.parse(str); },
81
+ "object": function(str) { return JSON.parse(str); },
82
+ "auto": function(str) { return $.serializeJSON.parseValue(str, null, null, {parseNumbers: true, parseBooleans: true, parseNulls: true}); }, // try again with something like "parseAll"
83
+ "skip": null // skip is a special type that makes it easy to ignore elements
84
+ },
85
+
86
+ useIntKeysAsArrayIndex: false // name="foo[2]" value="v" => {foo: [null, null, "v"]}, instead of {foo: ["2": "v"]}
87
+ },
88
+
89
+ // Merge option defaults into the options
90
+ setupOpts: function(options) {
91
+ var opt, validOpts, defaultOptions, optWithDefault, parseAll, f;
92
+ f = $.serializeJSON;
93
+
94
+ if (options == null) { options = {}; } // options ||= {}
95
+ defaultOptions = f.defaultOptions || {}; // defaultOptions
96
+
97
+ // Make sure that the user didn't misspell an option
98
+ validOpts = ['checkboxUncheckedValue', 'parseNumbers', 'parseBooleans', 'parseNulls', 'parseAll', 'parseWithFunction', 'skipFalsyValuesForTypes', 'skipFalsyValuesForFields', 'customTypes', 'defaultTypes', 'useIntKeysAsArrayIndex']; // re-define because the user may override the defaultOptions
99
+ for (opt in options) {
100
+ if (validOpts.indexOf(opt) === -1) {
101
+ throw new Error("serializeJSON ERROR: invalid option '" + opt + "'. Please use one of " + validOpts.join(', '));
102
+ }
103
+ }
104
+
105
+ // Helper to get the default value for this option if none is specified by the user
106
+ optWithDefault = function(key) { return (options[key] !== false) && (options[key] !== '') && (options[key] || defaultOptions[key]); };
107
+
108
+ // Return computed options (opts to be used in the rest of the script)
109
+ parseAll = optWithDefault('parseAll');
110
+ return {
111
+ checkboxUncheckedValue: optWithDefault('checkboxUncheckedValue'),
112
+
113
+ parseNumbers: parseAll || optWithDefault('parseNumbers'),
114
+ parseBooleans: parseAll || optWithDefault('parseBooleans'),
115
+ parseNulls: parseAll || optWithDefault('parseNulls'),
116
+ parseWithFunction: optWithDefault('parseWithFunction'),
117
+
118
+ skipFalsyValuesForTypes: optWithDefault('skipFalsyValuesForTypes'),
119
+ skipFalsyValuesForFields: optWithDefault('skipFalsyValuesForFields'),
120
+ typeFunctions: $.extend({}, optWithDefault('defaultTypes'), optWithDefault('customTypes')),
121
+
122
+ useIntKeysAsArrayIndex: optWithDefault('useIntKeysAsArrayIndex')
123
+ };
124
+ },
125
+
126
+ // Given a string, apply the type or the relevant "parse" options, to return the parsed value
127
+ parseValue: function(valStr, inputName, type, opts) {
128
+ var f, parsedVal;
129
+ f = $.serializeJSON;
130
+ parsedVal = valStr; // if no parsing is needed, the returned value will be the same
131
+
132
+ if (opts.typeFunctions && type && opts.typeFunctions[type]) { // use a type if available
133
+ parsedVal = opts.typeFunctions[type](valStr);
134
+ } else if (opts.parseNumbers && f.isNumeric(valStr)) { // auto: number
135
+ parsedVal = Number(valStr);
136
+ } else if (opts.parseBooleans && (valStr === "true" || valStr === "false")) { // auto: boolean
137
+ parsedVal = (valStr === "true");
138
+ } else if (opts.parseNulls && valStr == "null") { // auto: null
139
+ parsedVal = null;
140
+ } else if (opts.typeFunctions && opts.typeFunctions["string"]) { // make sure to apply :string type if it was re-defined
141
+ parsedVal = opts.typeFunctions["string"](valStr);
142
+ }
143
+
144
+ // Custom parse function: apply after parsing options, unless there's an explicit type.
145
+ if (opts.parseWithFunction && !type) {
146
+ parsedVal = opts.parseWithFunction(parsedVal, inputName);
147
+ }
148
+
149
+ return parsedVal;
150
+ },
151
+
152
+ isObject: function(obj) { return obj === Object(obj); }, // is it an Object?
153
+ isUndefined: function(obj) { return obj === void 0; }, // safe check for undefined values
154
+ isValidArrayIndex: function(val) { return /^[0-9]+$/.test(String(val)); }, // 1,2,3,4 ... are valid array indexes
155
+ isNumeric: function(obj) { return obj - parseFloat(obj) >= 0; }, // taken from jQuery.isNumeric implementation. Not using jQuery.isNumeric to support old jQuery and Zepto versions
156
+
157
+ optionKeys: function(obj) { if (Object.keys) { return Object.keys(obj); } else { var key, keys = []; for(key in obj){ keys.push(key); } return keys;} }, // polyfill Object.keys to get option keys in IE<9
158
+
159
+
160
+ // Fill the formAsArray object with values for the unchecked checkbox inputs,
161
+ // using the same format as the jquery.serializeArray function.
162
+ // The value of the unchecked values is determined from the opts.checkboxUncheckedValue
163
+ // and/or the data-unchecked-value attribute of the inputs.
164
+ readCheckboxUncheckedValues: function (formAsArray, opts, $form) {
165
+ var selector, $uncheckedCheckboxes, $el, uncheckedValue, f, name;
166
+ if (opts == null) { opts = {}; }
167
+ f = $.serializeJSON;
168
+
169
+ selector = 'input[type=checkbox][name]:not(:checked):not([disabled])';
170
+ $uncheckedCheckboxes = $form.find(selector).add($form.filter(selector));
171
+ $uncheckedCheckboxes.each(function (i, el) {
172
+ // Check data attr first, then the option
173
+ $el = $(el);
174
+ uncheckedValue = $el.attr('data-unchecked-value');
175
+ if (uncheckedValue == null) {
176
+ uncheckedValue = opts.checkboxUncheckedValue;
177
+ }
178
+
179
+ // If there's an uncheckedValue, push it into the serialized formAsArray
180
+ if (uncheckedValue != null) {
181
+ if (el.name && el.name.indexOf("[][") !== -1) { // identify a non-supported
182
+ throw new Error("serializeJSON ERROR: checkbox unchecked values are not supported on nested arrays of objects like '"+el.name+"'. See https://github.com/marioizquierdo/jquery.serializeJSON/issues/67");
183
+ }
184
+ formAsArray.push({name: el.name, value: uncheckedValue});
185
+ }
186
+ });
187
+ },
188
+
189
+ // Returns and object with properties {name_without_type, type} from a given name.
190
+ // The type is null if none specified. Example:
191
+ // "foo" => {nameWithNoType: "foo", type: null}
192
+ // "foo:boolean" => {nameWithNoType: "foo", type: "boolean"}
193
+ // "foo[bar]:null" => {nameWithNoType: "foo[bar]", type: "null"}
194
+ extractTypeAndNameWithNoType: function(name) {
195
+ var match;
196
+ if (match = name.match(/(.*):([^:]+)$/)) {
197
+ return {nameWithNoType: match[1], type: match[2]};
198
+ } else {
199
+ return {nameWithNoType: name, type: null};
200
+ }
201
+ },
202
+
203
+
204
+ // Check if this input should be skipped when it has a falsy value,
205
+ // depending on the options to skip values by name or type, and the data-skip-falsy attribute.
206
+ shouldSkipFalsy: function($form, name, nameWithNoType, type, opts) {
207
+ var f = $.serializeJSON;
208
+
209
+ var skipFromDataAttr = f.attrFromInputWithName($form, name, 'data-skip-falsy');
210
+ if (skipFromDataAttr != null) {
211
+ return skipFromDataAttr !== 'false'; // any value is true, except if explicitly using 'false'
212
+ }
213
+
214
+ var optForFields = opts.skipFalsyValuesForFields;
215
+ if (optForFields && (optForFields.indexOf(nameWithNoType) !== -1 || optForFields.indexOf(name) !== -1)) {
216
+ return true;
217
+ }
218
+
219
+ var optForTypes = opts.skipFalsyValuesForTypes;
220
+ if (type == null) type = 'string'; // assume fields with no type are targeted as string
221
+ if (optForTypes && optForTypes.indexOf(type) !== -1) {
222
+ return true
223
+ }
224
+
225
+ return false;
226
+ },
227
+
228
+ // Finds the first input in $form with this name, and get the given attr from it.
229
+ // Returns undefined if no input or no attribute was found.
230
+ attrFromInputWithName: function($form, name, attrName) {
231
+ var escapedName, selector, $input, attrValue;
232
+ escapedName = name.replace(/(:|\.|\[|\]|\s)/g,'\\$1'); // every non-standard character need to be escaped by \\
233
+ selector = '[name="' + escapedName + '"]';
234
+ $input = $form.find(selector).add($form.filter(selector)); // NOTE: this returns only the first $input element if multiple are matched with the same name (i.e. an "array[]"). So, arrays with different element types specified through the data-value-type attr is not supported.
235
+ return $input.attr(attrName);
236
+ },
237
+
238
+ // Raise an error if the type is not recognized.
239
+ validateType: function(name, type, opts) {
240
+ var validTypes, f;
241
+ f = $.serializeJSON;
242
+ validTypes = f.optionKeys(opts ? opts.typeFunctions : f.defaultOptions.defaultTypes);
243
+ if (!type || validTypes.indexOf(type) !== -1) {
244
+ return true;
245
+ } else {
246
+ throw new Error("serializeJSON ERROR: Invalid type " + type + " found in input name '" + name + "', please use one of " + validTypes.join(', '));
247
+ }
248
+ },
249
+
250
+
251
+ // Split the input name in programatically readable keys.
252
+ // Examples:
253
+ // "foo" => ['foo']
254
+ // "[foo]" => ['foo']
255
+ // "foo[inn][bar]" => ['foo', 'inn', 'bar']
256
+ // "foo[inn[bar]]" => ['foo', 'inn', 'bar']
257
+ // "foo[inn][arr][0]" => ['foo', 'inn', 'arr', '0']
258
+ // "arr[][val]" => ['arr', '', 'val']
259
+ splitInputNameIntoKeysArray: function(nameWithNoType) {
260
+ var keys, f;
261
+ f = $.serializeJSON;
262
+ keys = nameWithNoType.split('['); // split string into array
263
+ keys = $.map(keys, function (key) { return key.replace(/\]/g, ''); }); // remove closing brackets
264
+ if (keys[0] === '') { keys.shift(); } // ensure no opening bracket ("[foo][inn]" should be same as "foo[inn]")
265
+ return keys;
266
+ },
267
+
268
+ // Set a value in an object or array, using multiple keys to set in a nested object or array:
269
+ //
270
+ // deepSet(obj, ['foo'], v) // obj['foo'] = v
271
+ // deepSet(obj, ['foo', 'inn'], v) // obj['foo']['inn'] = v // Create the inner obj['foo'] object, if needed
272
+ // deepSet(obj, ['foo', 'inn', '123'], v) // obj['foo']['arr']['123'] = v //
273
+ //
274
+ // deepSet(obj, ['0'], v) // obj['0'] = v
275
+ // deepSet(arr, ['0'], v, {useIntKeysAsArrayIndex: true}) // arr[0] = v
276
+ // deepSet(arr, [''], v) // arr.push(v)
277
+ // deepSet(obj, ['arr', ''], v) // obj['arr'].push(v)
278
+ //
279
+ // arr = [];
280
+ // deepSet(arr, ['', v] // arr => [v]
281
+ // deepSet(arr, ['', 'foo'], v) // arr => [v, {foo: v}]
282
+ // deepSet(arr, ['', 'bar'], v) // arr => [v, {foo: v, bar: v}]
283
+ // deepSet(arr, ['', 'bar'], v) // arr => [v, {foo: v, bar: v}, {bar: v}]
284
+ //
285
+ deepSet: function (o, keys, value, opts) {
286
+ var key, nextKey, tail, lastIdx, lastVal, f;
287
+ if (opts == null) { opts = {}; }
288
+ f = $.serializeJSON;
289
+ if (f.isUndefined(o)) { throw new Error("ArgumentError: param 'o' expected to be an object or array, found undefined"); }
290
+ if (!keys || keys.length === 0) { throw new Error("ArgumentError: param 'keys' expected to be an array with least one element"); }
291
+
292
+ key = keys[0];
293
+
294
+ // Only one key, then it's not a deepSet, just assign the value.
295
+ if (keys.length === 1) {
296
+ if (key === '') {
297
+ o.push(value); // '' is used to push values into the array (assume o is an array)
298
+ } else {
299
+ o[key] = value; // other keys can be used as object keys or array indexes
300
+ }
301
+
302
+ // With more keys is a deepSet. Apply recursively.
303
+ } else {
304
+ nextKey = keys[1];
305
+
306
+ // '' is used to push values into the array,
307
+ // with nextKey, set the value into the same object, in object[nextKey].
308
+ // Covers the case of ['', 'foo'] and ['', 'var'] to push the object {foo, var}, and the case of nested arrays.
309
+ if (key === '') {
310
+ lastIdx = o.length - 1; // asume o is array
311
+ lastVal = o[lastIdx];
312
+ if (f.isObject(lastVal) && (f.isUndefined(lastVal[nextKey]) || keys.length > 2)) { // if nextKey is not present in the last object element, or there are more keys to deep set
313
+ key = lastIdx; // then set the new value in the same object element
314
+ } else {
315
+ key = lastIdx + 1; // otherwise, point to set the next index in the array
316
+ }
317
+ }
318
+
319
+ // '' is used to push values into the array "array[]"
320
+ if (nextKey === '') {
321
+ if (f.isUndefined(o[key]) || !$.isArray(o[key])) {
322
+ o[key] = []; // define (or override) as array to push values
323
+ }
324
+ } else {
325
+ if (opts.useIntKeysAsArrayIndex && f.isValidArrayIndex(nextKey)) { // if 1, 2, 3 ... then use an array, where nextKey is the index
326
+ if (f.isUndefined(o[key]) || !$.isArray(o[key])) {
327
+ o[key] = []; // define (or override) as array, to insert values using int keys as array indexes
328
+ }
329
+ } else { // for anything else, use an object, where nextKey is going to be the attribute name
330
+ if (f.isUndefined(o[key]) || !f.isObject(o[key])) {
331
+ o[key] = {}; // define (or override) as object, to set nested properties
332
+ }
333
+ }
334
+ }
335
+
336
+ // Recursively set the inner object
337
+ tail = keys.slice(1);
338
+ f.deepSet(o[key], tail, value, opts);
339
+ }
340
+ }
341
+
342
+ };
343
+
344
+ }));