fae-rails 2.2.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -1
  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 +1 -1
  6. data/app/assets/javascripts/fae/application.js +5 -1
  7. data/app/assets/javascripts/fae/form/_ajax.js +14 -4
  8. data/app/assets/javascripts/fae/form/_form.js +4 -2
  9. data/app/assets/javascripts/fae/form/_validator.js +224 -55
  10. data/app/assets/javascripts/fae/form/drag_drop.js +109 -0
  11. data/app/assets/javascripts/fae/form/inputs/_select.js +7 -4
  12. data/app/assets/javascripts/fae/form/inputs/_text.js +23 -9
  13. data/app/assets/javascripts/fae/vendor/simplemde/codemirror-4.inline-attachment.js +95 -0
  14. data/app/assets/javascripts/fae/vendor/simplemde/inline-attachment.js +405 -0
  15. data/app/assets/stylesheets/fae/application.css +1 -0
  16. data/app/assets/stylesheets/fae/base.scss +2 -1
  17. data/app/assets/stylesheets/fae/globals/_tags.scss +1 -1
  18. data/app/assets/stylesheets/fae/globals/layout/_base.scss +2 -2
  19. data/app/assets/stylesheets/fae/globals/layout/_content-header.scss +6 -2
  20. data/app/assets/stylesheets/fae/globals/legacy/_pre-1.3.scss +1 -1
  21. data/app/assets/stylesheets/fae/globals/navigation/_footer.scss +1 -1
  22. data/app/assets/stylesheets/fae/globals/navigation/_header.scss +2 -2
  23. data/app/assets/stylesheets/fae/globals/navigation/_sidenav.scss +2 -2
  24. data/app/assets/stylesheets/fae/globals/navigation/_utility.scss +1 -1
  25. data/app/assets/stylesheets/fae/modules/_errors-bar.scss +19 -0
  26. data/app/assets/stylesheets/fae/modules/_toggles.scss +1 -1
  27. data/app/assets/stylesheets/fae/modules/forms/_base.scss +13 -0
  28. data/app/assets/stylesheets/fae/modules/forms/_date.scss +4 -4
  29. data/app/assets/stylesheets/fae/modules/forms/_hints.scss +1 -1
  30. data/app/assets/stylesheets/fae/modules/forms/_label.scss +1 -1
  31. data/app/assets/stylesheets/fae/modules/forms/_select.scss +2 -2
  32. data/app/assets/stylesheets/fae/modules/forms/_simple-mde.scss +71 -0
  33. data/app/assets/stylesheets/fae/modules/tables/_base.scss +1 -1
  34. data/app/assets/stylesheets/fae/modules/tables/_pagination.scss +2 -2
  35. data/app/assets/stylesheets/fae/pages/_error.scss +1 -1
  36. data/app/assets/stylesheets/fae/pages/_login.scss +1 -1
  37. data/app/assets/stylesheets/fae/simplemde_override.scss +32 -0
  38. data/app/controllers/fae/application_controller.rb +2 -2
  39. data/app/controllers/fae/base_controller.rb +1 -1
  40. data/app/controllers/fae/deploy_hooks_controller.rb +4 -4
  41. data/app/controllers/fae/setup_controller.rb +2 -2
  42. data/app/helpers/fae/application_helper.rb +10 -1
  43. data/app/helpers/fae/view_helper.rb +4 -0
  44. data/app/models/concerns/fae/base_model_concern.rb +8 -0
  45. data/app/models/concerns/fae/seo_set_concern.rb +1 -0
  46. data/app/models/fae/change.rb +1 -0
  47. data/app/models/fae/option.rb +1 -0
  48. data/app/models/fae/seo_set.rb +14 -0
  49. data/app/services/fae/netlify_api.rb +22 -6
  50. data/app/uploaders/fae/file_uploader.rb +1 -1
  51. data/app/uploaders/fae/image_uploader.rb +2 -2
  52. data/app/views/fae/application/_header.slim +2 -2
  53. data/app/views/fae/application/_seo_set_form.html.slim +12 -0
  54. data/app/views/fae/options/_form.html.slim +2 -1
  55. data/app/views/fae/pages/home.html.slim +1 -1
  56. data/app/views/fae/shared/_errors.slim +0 -3
  57. data/app/views/fae/shared/_form_header.html.slim +4 -0
  58. data/app/views/fae/shared/_nested_table.html.slim +2 -1
  59. data/app/views/fae/shared/_shared_nested_table.html.slim +5 -2
  60. data/app/views/layouts/fae/application.html.slim +1 -1
  61. data/config/initializers/carrierwave.rb +41 -2
  62. data/config/initializers/fae_judge.rb +4 -2
  63. data/config/locales/fae.en.yml +10 -1
  64. data/config/puma.rb +82 -0
  65. data/db/migrate/20221118161833_create_fae_seo_sets.rb +13 -0
  66. data/lib/fae/concerns/models/base.rb +2 -0
  67. data/lib/fae/engine.rb +3 -3
  68. data/lib/fae/options.rb +17 -19
  69. data/lib/fae/version.rb +1 -1
  70. data/lib/generators/fae/base_generator.rb +17 -4
  71. data/lib/generators/fae/controller_generator.rb +0 -1
  72. data/lib/generators/fae/install_generator.rb +1 -1
  73. data/lib/generators/fae/model_generator.rb +1 -2
  74. data/lib/generators/fae/nested_index_scaffold_generator.rb +1 -2
  75. data/lib/generators/fae/nested_scaffold_generator.rb +13 -5
  76. data/lib/generators/fae/page_generator.rb +1 -2
  77. data/lib/generators/fae/scaffold_generator.rb +1 -1
  78. data/lib/generators/fae/templates/controllers/nested_scaffold_controller.rb +1 -3
  79. data/lib/generators/fae/templates/controllers/scaffold_controller.rb +7 -0
  80. data/lib/generators/fae/templates/views/_form.html.slim +2 -0
  81. data/lib/generators/fae/templates/views/_form_nested.html.slim +3 -0
  82. metadata +29 -20
  83. data/config/deploy/dev.rb +0 -19
  84. data/config/deploy/prod.rb +0 -19
  85. data/config/deploy/stage.rb +0 -19
  86. /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,14 +92,17 @@ Fae.form.select = {
92
92
  })
93
93
  });
94
94
 
95
- // prevent duplicate link from being added when nested form re-renders on the page after saving
96
- if (!$('.js-multiselect-action-deselect_all').length > 0) {
95
+ // prevent multiple deselect all actions from being added when nested forms are generated
96
+ if ($('.multiselect-action_wrap').length === 0) {
97
97
  // Add actions to wraper
98
98
  $deselect_all_action.insertAfter($chosen);
99
99
  }
100
100
 
101
- // Add special "Select All" option and notify Chosen of new option
102
- 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
+ }
103
106
 
104
107
  // Mark label wrapper as having multiselect actions for styling
105
108
  $label.addClass('has-multiselect-actions');