fae-rails 2.1.0 → 3.0.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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -2
  3. data/app/assets/config/fae/manifest.js +2 -0
  4. data/app/assets/javascripts/fae/_contrast.js +50 -0
  5. data/app/assets/javascripts/fae/_deploy.js +198 -0
  6. data/app/assets/javascripts/fae/_modals.js +94 -0
  7. data/app/assets/javascripts/fae/application.js +7 -1
  8. data/app/assets/javascripts/fae/form/_ajax.js +17 -5
  9. data/app/assets/javascripts/fae/form/_filtering.js +34 -0
  10. data/app/assets/javascripts/fae/form/_form.js +5 -2
  11. data/app/assets/javascripts/fae/form/_form_manager.js +295 -0
  12. data/app/assets/javascripts/fae/form/_slugger.js.erb +2 -2
  13. data/app/assets/javascripts/fae/form/_validator.js +224 -55
  14. data/app/assets/javascripts/fae/form/drag_drop.js +109 -0
  15. data/app/assets/javascripts/fae/form/inputs/_select.js +10 -4
  16. data/app/assets/javascripts/fae/form/inputs/_text.js +23 -9
  17. data/app/assets/javascripts/fae/vendor/simplemde/codemirror-4.inline-attachment.js +95 -0
  18. data/app/assets/javascripts/fae/vendor/simplemde/inline-attachment.js +405 -0
  19. data/app/assets/stylesheets/fae/application.css +1 -0
  20. data/app/assets/stylesheets/fae/base.scss +7 -3
  21. data/app/assets/stylesheets/fae/globals/_tags.scss +1 -1
  22. data/app/assets/stylesheets/fae/globals/imports/_variables.scss +1 -0
  23. data/app/assets/stylesheets/fae/globals/layout/_base.scss +9 -4
  24. data/app/assets/stylesheets/fae/globals/layout/_content-header.scss +14 -4
  25. data/app/assets/stylesheets/fae/globals/legacy/_pre-1.3.scss +1 -1
  26. data/app/assets/stylesheets/fae/globals/navigation/_footer.scss +1 -1
  27. data/app/assets/stylesheets/fae/globals/navigation/_header.scss +3 -3
  28. data/app/assets/stylesheets/fae/globals/navigation/_multi-col-subnav.scss +50 -0
  29. data/app/assets/stylesheets/fae/globals/navigation/_sidenav.scss +2 -2
  30. data/app/assets/stylesheets/fae/globals/navigation/_utility.scss +1 -1
  31. data/app/assets/stylesheets/fae/modules/_buttons.scss +11 -0
  32. data/app/assets/stylesheets/fae/modules/_deploy.scss +25 -0
  33. data/app/assets/stylesheets/fae/modules/_errors-bar.scss +19 -0
  34. data/app/assets/stylesheets/fae/modules/_modal.scss +25 -0
  35. data/app/assets/stylesheets/fae/modules/_toggles.scss +39 -23
  36. data/app/assets/stylesheets/fae/modules/forms/_asset-actions.scss +1 -1
  37. data/app/assets/stylesheets/fae/modules/forms/_base.scss +14 -1
  38. data/app/assets/stylesheets/fae/modules/forms/_checkbox.scss +1 -1
  39. data/app/assets/stylesheets/fae/modules/forms/_date.scss +20 -4
  40. data/app/assets/stylesheets/fae/modules/forms/_form-manager.scss +82 -0
  41. data/app/assets/stylesheets/fae/modules/forms/_hints.scss +1 -1
  42. data/app/assets/stylesheets/fae/modules/forms/_label.scss +1 -1
  43. data/app/assets/stylesheets/fae/modules/forms/_radio.scss +1 -1
  44. data/app/assets/stylesheets/fae/modules/forms/_select.scss +9 -8
  45. data/app/assets/stylesheets/fae/modules/forms/_simple-mde.scss +72 -1
  46. data/app/assets/stylesheets/fae/modules/forms/_textarea.scss +1 -1
  47. data/app/assets/stylesheets/fae/modules/tables/_base.scss +1 -1
  48. data/app/assets/stylesheets/fae/modules/tables/_filters.scss +4 -0
  49. data/app/assets/stylesheets/fae/modules/tables/_pagination.scss +2 -2
  50. data/app/assets/stylesheets/fae/pages/_error.scss +1 -1
  51. data/app/assets/stylesheets/fae/pages/_login.scss +5 -1
  52. data/app/assets/stylesheets/fae/simplemde_override.scss +32 -0
  53. data/app/controllers/fae/application_controller.rb +9 -2
  54. data/app/controllers/fae/base_controller.rb +6 -2
  55. data/app/controllers/fae/deploy_controller.rb +24 -0
  56. data/app/controllers/fae/deploy_hooks_controller.rb +71 -0
  57. data/app/controllers/fae/form_managers_controller.rb +19 -0
  58. data/app/controllers/fae/options_controller.rb +1 -0
  59. data/app/controllers/fae/setup_controller.rb +2 -2
  60. data/app/controllers/fae/static_pages_controller.rb +6 -1
  61. data/app/controllers/fae/users_controller.rb +11 -1
  62. data/app/controllers/fae/utilities_controller.rb +18 -6
  63. data/app/helpers/fae/application_helper.rb +36 -2
  64. data/app/helpers/fae/form_helper.rb +26 -2
  65. data/app/helpers/fae/view_helper.rb +26 -9
  66. data/app/models/concerns/fae/base_model_concern.rb +17 -0
  67. data/app/models/concerns/fae/seo_set_concern.rb +1 -0
  68. data/app/models/fae/change.rb +20 -6
  69. data/app/models/fae/deploy_hook.rb +12 -0
  70. data/app/models/fae/form_manager.rb +24 -0
  71. data/app/models/fae/option.rb +1 -0
  72. data/app/models/fae/seo_set.rb +14 -0
  73. data/app/models/fae/user.rb +2 -2
  74. data/app/services/fae/netlify_api.rb +213 -0
  75. data/app/uploaders/fae/file_uploader.rb +1 -1
  76. data/app/uploaders/fae/image_uploader.rb +2 -2
  77. data/app/views/devise/unlocks/new.html.slim +5 -9
  78. data/app/views/fae/application/_content_form.html.slim +16 -11
  79. data/app/views/fae/application/_file_uploader.html.slim +7 -2
  80. data/app/views/fae/application/_global_search_results.html.slim +1 -1
  81. data/app/views/fae/application/_header.slim +4 -1
  82. data/app/views/fae/application/_mobilenav.slim +3 -0
  83. data/app/views/fae/application/_seo_set_form.html.slim +12 -0
  84. data/app/views/fae/deploy/index.html.slim +40 -0
  85. data/app/views/fae/deploy_hooks/_form.html.slim +18 -0
  86. data/app/views/fae/deploy_hooks/_table.html.slim +28 -0
  87. data/app/views/fae/deploy_hooks/edit.html.slim +3 -0
  88. data/app/views/fae/deploy_hooks/new.html.slim +3 -0
  89. data/app/views/fae/images/_image_uploader.html.slim +12 -3
  90. data/app/views/fae/options/_form.html.slim +6 -2
  91. data/app/views/fae/pages/activity_log.html.slim +7 -3
  92. data/app/views/fae/pages/home.html.slim +3 -4
  93. data/app/views/fae/shared/_errors.slim +0 -3
  94. data/app/views/fae/shared/_form_header.html.slim +20 -12
  95. data/app/views/fae/shared/_nested_table.html.slim +5 -2
  96. data/app/views/fae/shared/_recent_changes.html.slim +1 -1
  97. data/app/views/fae/shared/_shared_nested_table.html.slim +9 -3
  98. data/app/views/layouts/fae/application.html.slim +2 -1
  99. data/config/deploy.rb +3 -1
  100. data/config/initializers/carrierwave.rb +41 -2
  101. data/config/initializers/devise.rb +6 -6
  102. data/config/initializers/fae_judge.rb +4 -2
  103. data/config/locales/fae.en.yml +49 -4
  104. data/config/locales/fae.zh-CN.yml +2 -2
  105. data/config/puma.rb +82 -0
  106. data/config/routes.rb +9 -1
  107. data/db/migrate/20140809222030_add_user_table.rb +1 -1
  108. data/db/migrate/20190925153222_create_fae_form_managers.rb +11 -0
  109. data/db/migrate/20220118192729_create_fae_publish_hooks.rb +10 -0
  110. data/db/migrate/20220128133730_rename_publish_hooks.rb +5 -0
  111. data/db/migrate/20220202153607_add_position_to_deploy_hooks.rb +6 -0
  112. data/db/migrate/20221118161833_create_fae_seo_sets.rb +13 -0
  113. data/lib/fae/concerns/models/base.rb +2 -0
  114. data/lib/fae/engine.rb +3 -3
  115. data/lib/fae/options.rb +18 -18
  116. data/lib/fae/version.rb +1 -1
  117. data/lib/generators/fae/base_generator.rb +28 -5
  118. data/lib/generators/fae/controller_generator.rb +0 -1
  119. data/lib/generators/fae/install_generator.rb +1 -1
  120. data/lib/generators/fae/model_generator.rb +1 -2
  121. data/lib/generators/fae/nested_index_scaffold_generator.rb +1 -2
  122. data/lib/generators/fae/nested_scaffold_generator.rb +23 -2
  123. data/lib/generators/fae/page_generator.rb +1 -2
  124. data/lib/generators/fae/scaffold_generator.rb +1 -1
  125. data/lib/generators/fae/templates/assets/fae.js +1 -1
  126. data/lib/generators/fae/templates/controllers/nested_scaffold_controller.rb +13 -1
  127. data/lib/generators/fae/templates/controllers/scaffold_controller.rb +7 -0
  128. data/lib/generators/fae/templates/initializers/fae.rb +16 -1
  129. data/lib/generators/fae/templates/views/_form.html.slim +12 -1
  130. data/lib/generators/fae/templates/views/_form_index_nested.html.slim +15 -1
  131. data/lib/generators/fae/templates/views/_form_nested.html.slim +22 -2
  132. data/lib/generators/fae/templates/views/static_page_form.html.slim +13 -1
  133. metadata +53 -24
  134. data/config/deploy/dev.rb +0 -19
  135. data/config/deploy/prod.rb +0 -19
  136. data/config/deploy/stage.rb +0 -19
  137. /data/app/assets/javascripts/fae/vendor/{simplemde.min.js → simplemde/simplemde.min.js} +0 -0
@@ -6,11 +6,11 @@
6
6
  * @memberof form
7
7
  */
8
8
  Fae.form.validator = {
9
-
10
9
  is_valid: '',
11
10
  validations_called: 0,
12
11
  validations_returned: 0,
13
12
  validation_test_count: 0,
13
+ invalidFields: [],
14
14
 
15
15
  init: function () {
16
16
  // validate all forms except the login form
@@ -20,6 +20,7 @@ Fae.form.validator = {
20
20
  this.bindValidationEvents();
21
21
  this.formValidate();
22
22
  this.length_counter.init();
23
+ this.checkForSsrImageAndFileErrors();
23
24
  }
24
25
  },
25
26
 
@@ -29,14 +30,13 @@ Fae.form.validator = {
29
30
  formValidate: function ($scope) {
30
31
  var _this = this;
31
32
 
32
- if (typeof($scope) === 'undefined'){
33
+ if (typeof $scope === 'undefined') {
33
34
  $scope = FCH.$document;
34
35
  }
35
36
  $scope.on('submit', 'form:not([data-remote=true])', function (e) {
36
37
  var $this = $(this);
37
38
 
38
39
  if ($this.data('passed_validation') !== 'true') {
39
-
40
40
  // pause form submission
41
41
  e.preventDefault();
42
42
 
@@ -63,8 +63,14 @@ Fae.form.validator = {
63
63
 
64
64
  _this.testValidation($this, $scope);
65
65
 
66
+ } else if (_this._preventFormSaveDueToNestedForm()) {
67
+ // nested form has unsaved changes and user cancelled form submission
68
+ e.preventDefault();
69
+
70
+ } else {
71
+ // form is valid and can submit so set saving indication
72
+ _this._setSavingIndicator();
66
73
  }
67
-
68
74
  });
69
75
  },
70
76
 
@@ -72,15 +78,13 @@ Fae.form.validator = {
72
78
  * Tests a forms validation after all validation checks have responded
73
79
  * Polls validations responses every 50ms to allow uniqueness AJAX calls to complete
74
80
  */
75
- testValidation: function($this, $scope) {
81
+ testValidation: function ($this, $scope) {
76
82
  var _this = this;
77
83
  _this.validation_test_count++;
78
84
 
79
- setTimeout(function(){
80
-
85
+ setTimeout(function () {
81
86
  // if all the validation checks have returned a response
82
87
  if (_this.validations_called === _this.validations_returned) {
83
-
84
88
  if (_this.is_valid) {
85
89
  // if form is valid, submit it
86
90
  $this.data('passed_validation', 'true');
@@ -89,15 +93,17 @@ Fae.form.validator = {
89
93
  } else {
90
94
  // otherwise scroll to the top to display alerts (unless in a nested form scope)
91
95
  Fae.navigation.language.checkForHiddenErrors();
92
- if (typeof($scope) === 'undefined') {
96
+ if (typeof $scope === 'undefined') {
93
97
  FCH.smoothScroll($('#js-main-header'), 500, 100, 0);
94
98
  }
95
99
 
96
- if ($(".field_with_errors").length) {
97
- $('.alert').slideDown('fast').delay(3000).slideUp('fast');
100
+ // display error messages grouped at top of form with jump links to invalid fields
101
+ _this._buildErrorLinks($scope);
102
+
103
+ if ($('.field_with_errors').length) {
104
+ $('.errors-bar-wrapper').slideDown('fast');
98
105
  }
99
106
  }
100
-
101
107
  } else {
102
108
  // check again if it hasn't run more than 50 times
103
109
  // (to prevent against infinite loop)
@@ -105,9 +111,7 @@ Fae.form.validator = {
105
111
  _this.testValidation($this);
106
112
  }
107
113
  }
108
-
109
114
  }, 50);
110
-
111
115
  },
112
116
 
113
117
  /**
@@ -116,7 +120,7 @@ Fae.form.validator = {
116
120
  bindValidationEvents: function ($scope) {
117
121
  var _this = this;
118
122
 
119
- if (typeof($scope) === 'undefined'){
123
+ if (typeof $scope === 'undefined') {
120
124
  $scope = $('body');
121
125
  }
122
126
 
@@ -129,13 +133,13 @@ Fae.form.validator = {
129
133
  $this.blur(function () {
130
134
  _this._judgeIt($this);
131
135
  });
132
-
133
136
  } else if ($this.hasClass('hasDatepicker')) {
134
137
  // date pickers need a little delay
135
138
  $this.blur(function () {
136
- setTimeout(function(){ _this._judgeIt($this); }, 500);
139
+ setTimeout(function () {
140
+ _this._judgeIt($this);
141
+ }, 500);
137
142
  });
138
-
139
143
  } else if ($this.is('select')) {
140
144
  // selects validate on change
141
145
  $this.change(function () {
@@ -146,6 +150,67 @@ Fae.form.validator = {
146
150
  });
147
151
  },
148
152
 
153
+ /**
154
+ * Sent indicator to let the user know the form is saving
155
+ * @protected
156
+ */
157
+ _setSavingIndicator: function () {
158
+ var $saveButton = $('.js-content-header').find('input[type="submit"]');
159
+ $saveButton.addClass('saving').val('Saving...');
160
+ },
161
+
162
+ /**
163
+ * Detect if a nested form has content and alert user
164
+ * @protected
165
+ */
166
+ _preventFormSaveDueToNestedForm: function () {
167
+ let preventSave = false;
168
+ const $nestedFormWrapper = $('.js-addedit-form-wrapper:visible');
169
+ // exit if no nested objects
170
+ if ($nestedFormWrapper.length === 0) return false;
171
+
172
+ const $form = $nestedFormWrapper.find('form');
173
+
174
+ // get all form values without hidden fields. this omits utf encoding, csrf token, and parent item id fields
175
+ const formValues = $form.find(':input:not(:hidden)').serializeArray();
176
+
177
+ // detect if any fields have content
178
+ const formHasContent = formValues.some((item) => item.value !== '');
179
+
180
+ if (formHasContent) {
181
+ const formLabel = $nestedFormWrapper.siblings('h2').text();
182
+ // set to true if user decides not to continue
183
+ preventSave = !window.confirm(
184
+ `${formLabel} has unsaved changes! To return to your draft, click “Cancel.” To proceed without saving, click “OK.”`
185
+ );
186
+
187
+ if (preventSave) {
188
+ FCH.smoothScroll($nestedFormWrapper, 500, 100, -100);
189
+ }
190
+ }
191
+
192
+ return preventSave;
193
+ },
194
+
195
+ // check for server side errors for images/files and display error bar with jump links on form re-render
196
+ checkForSsrImageAndFileErrors: function () {
197
+ const _this = this;
198
+ let imageErrorFound = false;
199
+
200
+ $('.input.file').each(function () {
201
+ $assetInput = $(this);
202
+ if ($assetInput.hasClass('field_with_errors')) {
203
+ const errorMessage = $assetInput.find('.error').text();
204
+ _this._addInvalidField($assetInput, [errorMessage]);
205
+ imageErrorFound = true;
206
+ }
207
+ });
208
+
209
+ if (imageErrorFound) {
210
+ this._buildErrorLinks();
211
+ $('.errors-bar-wrapper').slideDown('fast');
212
+ }
213
+ },
149
214
 
150
215
  /**
151
216
  * Initialize Judge on a field
@@ -159,6 +224,7 @@ Fae.form.validator = {
159
224
  valid: function () {
160
225
  _this.validations_returned++;
161
226
  _this._createSuccessClass($input);
227
+ _this._clearInvalidField($input);
162
228
  },
163
229
  invalid: function (input, messages) {
164
230
  _this.validations_returned++;
@@ -166,9 +232,95 @@ Fae.form.validator = {
166
232
  if (messages.length) {
167
233
  _this.is_valid = false;
168
234
  _this._createOrReplaceError($input, messages);
235
+ _this._addInvalidField($input, messages);
169
236
  }
170
- }
237
+ },
238
+ });
239
+ },
240
+
241
+ /**
242
+ * Add error to invalid fields array if it doesn't already exist
243
+ * @protected
244
+ * @param {jQuery} $input - field to check if exists
245
+ */
246
+ _addInvalidField: function ($input, messages) {
247
+ const foundIndex = this.invalidFields.findIndex((item) => {
248
+ return item.$input[0] === $input[0];
171
249
  });
250
+ if (foundIndex === -1) {
251
+ this.invalidFields.push({
252
+ $input: $input,
253
+ messages: messages,
254
+ });
255
+ }
256
+ },
257
+
258
+ /**
259
+ * Remove error from invalid fields array if exists
260
+ * @protected
261
+ * @param {jQuery} $input - field to check if exists
262
+ */
263
+ _clearInvalidField: function ($input) {
264
+ const foundIndex = this.invalidFields.findIndex((item) => {
265
+ return item.$input[0] === $input[0];
266
+ });
267
+ if (foundIndex !== -1) {
268
+ this.invalidFields.splice(foundIndex, 1);
269
+ }
270
+ },
271
+
272
+ /**
273
+ * Builds jump links for all invalid fields to display at top of form
274
+ * @protected
275
+ * @param {jQuery} $scope - form scope
276
+ */
277
+
278
+ _buildErrorLinks: function ($scope) {
279
+ const $header = $('.js-content-header');
280
+ const headerHeight = $header[0].getBoundingClientRect().height;
281
+
282
+ if (typeof $scope === 'undefined') {
283
+ $scope = $('body');
284
+ }
285
+
286
+ // get all fields with non-empty validations
287
+ const fieldsWithValidation = Array.from(
288
+ $scope.find('[data-validate*="{"]')
289
+ );
290
+
291
+ // sort invalid fields to match ordering of fields within form
292
+ this.invalidFields.sort((a, b) => {
293
+ return (
294
+ fieldsWithValidation.indexOf(a.$input[0]) -
295
+ fieldsWithValidation.indexOf(b.$input[0])
296
+ );
297
+ });
298
+
299
+ // generate error links
300
+ const $errorLinks = this.invalidFields.map((field) => {
301
+ const $wrapper = field.$input.parents('div.input');
302
+ let label = $wrapper.find('.label_inner').text();
303
+
304
+ // build clean label with name of field and error message
305
+ label = `${label.replace('*', '')} - ${field.messages.join(', ')}`;
306
+
307
+ // build jump link
308
+ let $errorLink = $('<a/>', {
309
+ class: 'error-jump-link',
310
+ href: `#`,
311
+ html: label,
312
+ });
313
+
314
+ // smooth scroll invalid field right below header
315
+ $errorLink.click((e) => {
316
+ e.preventDefault;
317
+ FCH.smoothScroll($wrapper, 500, 100, headerHeight * -1);
318
+ });
319
+ return $errorLink;
320
+ });
321
+
322
+ // append all links to error bar in form_header partial
323
+ $('.errors-bar').empty().append($errorLinks);
172
324
  },
173
325
 
174
326
  /**
@@ -208,7 +360,11 @@ Fae.form.validator = {
208
360
  var $styled_input = this._setTargetInput($input);
209
361
  $styled_input.addClass('valid').removeClass('invalid');
210
362
 
211
- $input.parent().removeClass('field_with_errors').children('.error').remove();
363
+ $input
364
+ .parent()
365
+ .removeClass('field_with_errors')
366
+ .children('.error')
367
+ .remove();
212
368
  },
213
369
 
214
370
  /**
@@ -225,7 +381,9 @@ Fae.form.validator = {
225
381
  if ($wrapper.children('.error').length) {
226
382
  $wrapper.children('.error').text(messages.join(', '));
227
383
  } else {
228
- $wrapper.addClass('field_with_errors').append("<span class='error'>" + messages.join(', ') + "</span>");
384
+ $wrapper
385
+ .addClass('field_with_errors')
386
+ .append("<span class='error'>" + messages.join(', ') + '</span>');
229
387
  }
230
388
  },
231
389
 
@@ -239,17 +397,18 @@ Fae.form.validator = {
239
397
  var $styled_input = $input;
240
398
 
241
399
  // If field is a chosen input
242
- if ( $input.next('.chosen-container').length ) {
400
+ if ($input.next('.chosen-container').length) {
243
401
  if ($input.next('.chosen-container').find('.chosen-single').length) {
244
402
  $styled_input = $input.next('.chosen-container').find('.chosen-single');
245
-
246
- } else if ($input.next('.chosen-container').find('.chosen-choices').length) {
247
- $styled_input = $input.next('.chosen-container').find('.chosen-choices');
248
-
403
+ } else if (
404
+ $input.next('.chosen-container').find('.chosen-choices').length
405
+ ) {
406
+ $styled_input = $input
407
+ .next('.chosen-container')
408
+ .find('.chosen-choices');
249
409
  }
250
410
  } else if ($input.hasClass('mde-enabled')) {
251
411
  $styled_input = $input.siblings('.editor-toolbar, .CodeMirror-wrap');
252
-
253
412
  }
254
413
 
255
414
  return $styled_input;
@@ -260,13 +419,16 @@ Fae.form.validator = {
260
419
  * @param {jQuery} $field - Input fields
261
420
  * @param {String} kind - Type of validation (e.g. 'presence' or 'confirmation')
262
421
  */
263
- stripValidation: function($field, kind) {
422
+ stripValidation: function ($field, kind) {
264
423
  var validations = $field.data('validate');
265
424
 
266
425
  for (var i = 0; i < validations.length; i++) {
267
426
  // validation items can be strings or JSON objects
268
427
  // let's convert the strings to JSON so we're dealing with consistent types
269
- if (typeof validations[i] == 'string' || validations[i] instanceof String) {
428
+ if (
429
+ typeof validations[i] == 'string' ||
430
+ validations[i] instanceof String
431
+ ) {
270
432
  validations[i] = JSON.parse(validations[i]);
271
433
  }
272
434
 
@@ -289,14 +451,17 @@ Fae.form.validator = {
289
451
  * @memberof! validator
290
452
  */
291
453
  password_confirmation_validation: {
292
- init: function() {
454
+ init: function () {
293
455
  var _this = this;
294
456
 
295
457
  _this.$password_field = $('#user_password');
296
458
  _this.$password_confirmation_field = $('#user_password_confirmation');
297
459
 
298
460
  if (_this.$password_confirmation_field.length) {
299
- Fae.form.validator.stripValidation(_this.$password_field, 'confirmation');
461
+ Fae.form.validator.stripValidation(
462
+ _this.$password_field,
463
+ 'confirmation'
464
+ );
300
465
  _this.addCustomValidation();
301
466
  }
302
467
  },
@@ -304,7 +469,7 @@ Fae.form.validator = {
304
469
  /**
305
470
  * Validate password on blur and form submit; halt form execution if invalid
306
471
  */
307
- addCustomValidation: function() {
472
+ addCustomValidation: function () {
308
473
  var _this = this;
309
474
 
310
475
  /**
@@ -316,18 +481,24 @@ Fae.form.validator = {
316
481
  function validateConfirmation() {
317
482
  var validator = Fae.form.validator;
318
483
 
319
- if (_this.$password_field.val() === _this.$password_confirmation_field.val()) {
484
+ if (
485
+ _this.$password_field.val() ===
486
+ _this.$password_confirmation_field.val()
487
+ ) {
320
488
  validator._createSuccessClass(_this.$password_confirmation_field);
321
489
  } else {
322
490
  var message = ['must match Password'];
323
491
  validator.is_valid = false;
324
- validator._createOrReplaceError(_this.$password_confirmation_field, message);
492
+ validator._createOrReplaceError(
493
+ _this.$password_confirmation_field,
494
+ message
495
+ );
325
496
  }
326
497
  }
327
498
 
328
499
  this.$password_confirmation_field.on('blur', validateConfirmation);
329
500
 
330
- $('form').on('submit', function(ev) {
501
+ $('form').on('submit', function (ev) {
331
502
  _this.is_valid = true;
332
503
  validateConfirmation();
333
504
 
@@ -335,13 +506,13 @@ Fae.form.validator = {
335
506
  ev.preventDefault();
336
507
  }
337
508
  });
338
- }
509
+ },
339
510
  },
340
511
 
341
512
  /**
342
513
  * Judge always read the `on: :create` validations, so we need to strip the password presence validation on the user edit form
343
514
  */
344
- passwordPresenceConditional: function() {
515
+ passwordPresenceConditional: function () {
345
516
  var $edit_user_password = $('.edit_user #user_password');
346
517
  if ($edit_user_password.length) {
347
518
  this.stripValidation($edit_user_password, 'presence');
@@ -354,25 +525,24 @@ Fae.form.validator = {
354
525
  * @memberof! validator
355
526
  */
356
527
  length_counter: {
357
-
358
- init: function(){
528
+ init: function () {
359
529
  this.findLengthValidations();
360
530
  },
361
531
 
362
532
  /**
363
533
  * Add counter text to fields that validate based on character counts
364
534
  */
365
- findLengthValidations: function() {
535
+ findLengthValidations: function () {
366
536
  var _this = this;
367
537
 
368
538
  $('[data-validate]').each(function () {
369
539
  var $this = $(this);
370
540
 
371
- if ($this.data('validate').length ) {
541
+ if ($this.data('validate').length) {
372
542
  var validations = $this.data('validate');
373
543
 
374
- $.grep(validations, function(item){
375
- if (item.kind === 'length'){
544
+ $.grep(validations, function (item) {
545
+ if (item.kind === 'length') {
376
546
  $this.data('length-max', item.options.maximum);
377
547
  _this._setupCounter($this);
378
548
  }
@@ -386,17 +556,17 @@ Fae.form.validator = {
386
556
  * @access protected
387
557
  * @param {jQuery} $elem - Input field being counted
388
558
  */
389
- _setupCounter: function($elem) {
559
+ _setupCounter: function ($elem) {
390
560
  var _this = this;
391
561
 
392
562
  _this._createCounterDiv($elem);
393
563
  _this.updateCounter($elem);
394
564
 
395
565
  $elem
396
- .keyup(function() {
566
+ .keyup(function () {
397
567
  _this.updateCounter($elem);
398
568
  })
399
- .keypress(function(e) {
569
+ .keypress(function (e) {
400
570
  if (_this._charactersLeft($elem) <= 0) {
401
571
  if (e.keyCode !== 8 && e.keyCode !== 46) {
402
572
  e.preventDefault();
@@ -410,17 +580,17 @@ Fae.form.validator = {
410
580
  * @protected
411
581
  * @param {jQuery} $elem - Input field to evaluate
412
582
  */
413
- _createCounterDiv: function($elem) {
583
+ _createCounterDiv: function ($elem) {
414
584
  if ($elem.siblings('.counter').length === 0) {
415
- var text = "Maximum Characters: " + $elem.data('length-max');
585
+ var text = 'Maximum Characters: ' + $elem.data('length-max');
416
586
  text += " / <span class='characters-left'></span>";
417
587
 
418
588
  var $counter_div = $('<div />', {
419
589
  class: 'counter',
420
- html: '<p>' + text + '</p>'
590
+ html: '<p>' + text + '</p>',
421
591
  });
422
592
 
423
- $elem.parent().append( $counter_div );
593
+ $elem.parent().append($counter_div);
424
594
  }
425
595
  },
426
596
 
@@ -428,7 +598,7 @@ Fae.form.validator = {
428
598
  * Updates the counter count and class
429
599
  * @param {jQuery} $elem - Input field to evaluate
430
600
  */
431
- updateCounter: function($elem) {
601
+ updateCounter: function ($elem) {
432
602
  var $count_span = $elem.siblings('.counter').find('.characters-left');
433
603
  if ($count_span.length) {
434
604
  var current = this._charactersLeft($elem);
@@ -452,14 +622,13 @@ Fae.form.validator = {
452
622
  * @param {jQuery} $elem - Input field being counted
453
623
  * @return {integer} The number of characters left
454
624
  */
455
- _charactersLeft: function($elem) {
625
+ _charactersLeft: function ($elem) {
456
626
  var input_value = $elem.val();
457
627
  var current = $elem.data('length-max') - input_value.length;
458
628
  // Rails counts a newline as two characters, so let's make up for it here
459
629
  current -= (input_value.match(/\n/g) || []).length;
460
630
 
461
631
  return current;
462
- }
463
- }
464
-
632
+ },
633
+ },
465
634
  };
@@ -0,0 +1,109 @@
1
+ /* global Fae */
2
+
3
+ /**
4
+ * Fae form drag n drop uploads
5
+ * @namespace form.dragDrop
6
+ * @memberof form
7
+ */
8
+ Fae.form.dragDrop = {
9
+
10
+ init: function () {
11
+ this.fileInputs = document.querySelectorAll('input[type="file"]');
12
+ if (this.fileInputs.length === 0) return;
13
+
14
+ this.bindListeners();
15
+ },
16
+
17
+ bindListeners() {
18
+
19
+ Array.from(this.fileInputs).forEach(input => {
20
+ const container = input.closest('.input.field');
21
+
22
+ ['dragenter', 'dragover'].forEach((eventName) => {
23
+ container.addEventListener(eventName, this.highlight.bind(container));
24
+ });
25
+
26
+ ['dragleave', 'drop'].forEach((eventName) => {
27
+ container.addEventListener(eventName, this.unhighlight.bind(container));
28
+ });
29
+
30
+ // This needed to be bound via jquery since tests have to use a simulated jquery event and native event listeners do not pickup on jquery events
31
+ $(container).on('drop', this.handleDrop.bind(this, container));
32
+ })
33
+ },
34
+
35
+ highlight(e) {
36
+ e.stopPropagation();
37
+ e.preventDefault();
38
+ this.classList.add('highlight');
39
+ },
40
+
41
+ unhighlight(e) {
42
+ e.stopPropagation();
43
+ e.preventDefault();
44
+ this.classList.remove('highlight');
45
+ },
46
+
47
+ handleDrop(inputContainer, e) {
48
+ // return the original event from the jquery event
49
+ if (e.originalEvent) {
50
+ e = e.originalEvent;
51
+ }
52
+ const input = inputContainer.querySelector('input[type="file"]');
53
+ const fileList = e.dataTransfer.files;
54
+ const file = fileList[0];
55
+ const isValidFile = this.validatesFileSize(input, file);
56
+
57
+ if (isValidFile) {
58
+ this.attachFile(input, fileList);
59
+ this.addFileInfo(inputContainer, file);
60
+ }
61
+ },
62
+
63
+ attachFile(input, files) {
64
+ input.files = files;
65
+ },
66
+
67
+ addFileInfo(inputContainer, file) {
68
+ const deleteButton = inputContainer.querySelector('.asset-delete');
69
+
70
+ // only exists if image is already loaded into field
71
+ let label = inputContainer.querySelector('.asset-title');
72
+ if (!label) {
73
+ // else is a new image
74
+ label = inputContainer.querySelector('.asset-actions span');
75
+ }
76
+ label.innerText = file.name;
77
+ deleteButton.style.display = 'block';
78
+ },
79
+
80
+ validatesFileSize(input, file) {
81
+ const limit = parseInt(input.dataset.limit);
82
+ const fileSize = file.size / 1024 / 1024;
83
+
84
+ if (fileSize < limit) {
85
+ this.removeFileSizeError(input);
86
+ return true;
87
+ }
88
+
89
+ this.addFileSizeError(input, limit);
90
+ return false;
91
+ },
92
+
93
+ addFileSizeError(input, limit) {
94
+ const errorElem = document.createElement('span');
95
+ errorElem.innerText = input.dataset.exceeded.replace('###', limit);
96
+ errorElem.classList.add('error');
97
+ input.after(errorElem);
98
+ input.parentElement.classList.add('field_with_errors');
99
+ },
100
+
101
+ removeFileSizeError(input) {
102
+ const nextSibling = input.nextSibling;
103
+ if (nextSibling.classList.contains('error')) {
104
+ nextSibling.remove();
105
+ }
106
+ input.parentElement.classList.remove('field_with_errors');
107
+ }
108
+
109
+ };
@@ -92,11 +92,17 @@ Fae.form.select = {
92
92
  })
93
93
  });
94
94
 
95
- // Add actions to wraper
96
- $deselect_all_action.insertAfter($chosen);
95
+ // prevent multiple deselect all actions from being added when nested forms are generated
96
+ if ($('.multiselect-action_wrap').length === 0) {
97
+ // Add actions to wraper
98
+ $deselect_all_action.insertAfter($chosen);
99
+ }
97
100
 
98
- // Add special "Select All" option and notify Chosen of new option
99
- addSelectAllOption($element)
101
+ // prevent multiple 'SELECT ALL' options from being added when nested forms are generated
102
+ if ($element[0].options[0].value != select_all_value) {
103
+ // Add special "Select All" option and notify Chosen of new option
104
+ addSelectAllOption($element);
105
+ }
100
106
 
101
107
  // Mark label wrapper as having multiselect actions for styling
102
108
  $label.addClass('has-multiselect-actions');