cocooned 1.3.0 → 1.3.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d9d9a27c6eb59ebc51ae3ab92101a7cf66f6fc13e8e23b011a19ee8722dd083
4
- data.tar.gz: c258c388dc29c44d3b0dc9024d5423a7c118ef45f5549c168b61f536c55e9b8a
3
+ metadata.gz: 0ff5fc052a1bf37f18e1f22b550f35bf876e0cc09c4c7d4ebef3667c7afde67a
4
+ data.tar.gz: 55202ea14bda345f2ed5f20fc95ebb0de1bfa7831e9f70ba92b2a092c7ca210b
5
5
  SHA512:
6
- metadata.gz: a5f36777a4f62e29498b58491db235b55df6df2084e9a1cebdbbf0842501751e740ee4e996084d7092fe39da85609fb4575f23e64032a3a4207a5ddb5d7bf605
7
- data.tar.gz: a80571f97c5ffaf66aea6856b3ea0cc3ee1d403633e48b18cb6e1a464c93cf353cd6d10a4a1681c60bba9aa98f647922de74aac62e9a23172fbdf637fd3b77de
6
+ metadata.gz: ed672de23fce13326d63cb1e943689ea8f4df8be8873111daecb9a25df954ee06a47592a6b28399c95120e6079337c26125838b42a868395e0d5639d08461569
7
+ data.tar.gz: 23b8d0ab6bdfda55e974f322bec4a0a39a9fd089f9bfbfa343144ccdc76639560286a4212ac3018e0ee10a567d70b8f363be5c3c3578e5000440de41a2464e04
data/Rakefile CHANGED
@@ -1,44 +1,106 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/gem_tasks'
3
+ ## Test suites
4
4
 
5
- # Test suites
5
+ # Ruby
6
6
  require 'rspec/core/rake_task'
7
7
  RSpec::Core::RakeTask.new
8
-
9
8
  task default: :spec
10
9
 
10
+ # JavaScript
11
11
  require 'jasmine'
12
12
  load 'jasmine/tasks/jasmine.rake'
13
13
 
14
- # Linters
14
+ ## Linters
15
+
16
+ # Ruby
15
17
  require 'rubocop/rake_task'
16
18
  RuboCop::RakeTask.new do |task|
17
19
  task.options = ['--config', 'config/linters/ruby.yml']
18
20
  end
19
21
 
22
+ # JavaScript
20
23
  eslint_args = ['--no-eslintrc', '--config config/linters/js.json']
21
24
  eslint_path = ['app/assets/**/*.js', 'spec/javascripts/**/*.js', 'spec/dummy/app/assets/**/*.js']
22
25
 
23
26
  namespace :eslint do
24
- desc 'Auto-correct JavaScript files'
27
+ desc 'Auto-correct eslint offenses'
25
28
  task :auto_correct do
26
29
  system("yarnpkg run eslint #{(eslint_args + ['--fix']).join(' ')} #{eslint_path.join(' ')}")
27
30
  end
28
31
  end
29
32
 
30
- desc 'Lint JavaScript code'
33
+ desc 'Run eslint'
31
34
  task :eslint do
32
35
  system("yarnpkg run eslint #{eslint_args.join(' ')} #{eslint_path.join(' ')}")
33
36
  end
34
37
 
35
- # Documentation
36
- require 'rdoc/task'
38
+ # Both
39
+ namespace :linters do
40
+ desc 'Auto-correct Rubocop and eslint offenses'
41
+ task auto_correct: ['rubocop:auto_correct', 'eslint:auto_correct']
42
+ end
43
+
44
+ desc 'Run Rubocop and eslint'
45
+ task linters: %i[rubocop eslint]
46
+
47
+ ## Packaging
48
+
49
+ # Ruby
50
+ require 'bundler/gem_tasks'
51
+
52
+ # JavaScript
53
+ require 'json'
54
+
55
+ spec = Bundler.load_gemspec('./cocooned.gemspec')
56
+ npm_scope = 'notus.sh'
57
+
58
+ npm_src_dir = './npm'
59
+ npm_dest_dir = './dist'
60
+ CLOBBER.include 'dist'
37
61
 
38
- Rake::RDocTask.new(:rdoc) do |rdoc|
39
- rdoc.rdoc_dir = 'rdoc'
40
- rdoc.title = 'Cocooned'
41
- rdoc.options << '--line-numbers' << '--inline-source'
42
- rdoc.rdoc_files.include('README.rdoc')
43
- rdoc.rdoc_files.include('lib/**/*.rb')
62
+ assets_dir = './app/assets/'
63
+
64
+ npm_files = {
65
+ File.join(npm_dest_dir, 'cocooned.js') => File.join(assets_dir, 'javascripts', 'cocooned.js'),
66
+ File.join(npm_dest_dir, 'cocooned.css') => File.join(assets_dir, 'stylesheets', 'cocooned.css'),
67
+ File.join(npm_dest_dir, 'README.md') => File.join(npm_src_dir, 'README.md'),
68
+ File.join(npm_dest_dir, 'LICENSE') => './LICENSE'
69
+ }
70
+
71
+ namespace :npm do
72
+ npm_files.each do |dest, src|
73
+ file dest => src do
74
+ cp src, dest
75
+ end
76
+ end
77
+
78
+ task :'package-json' do
79
+ contributors = []
80
+ spec.authors.each_with_index do |name, i|
81
+ next if spec.email[i].nil?
82
+ contributors << {
83
+ name: name.dup.force_encoding('UTF-8'),
84
+ email: spec.email[i].dup.force_encoding('UTF-8')
85
+ }
86
+ end
87
+
88
+ template = ERB.new(File.read(File.join(npm_src_dir, 'package.json.erb')))
89
+ content = template.result_with_hash(scope: npm_scope, spec: spec, contributors: contributors)
90
+ File.write(File.join(npm_dest_dir, 'package.json'),
91
+ JSON.pretty_generate(JSON.parse(content)))
92
+ end
93
+
94
+ desc "Build #{npm_scope}-#{spec.name}-#{spec.version}.tgz into the pkg directory"
95
+ task build: (%i[package-json] + npm_files.keys) do
96
+ system("cd #{npm_dest_dir} && npm pack && mv ./#{npm_scope}-#{spec.name}-#{spec.version}.tgz ../pkg/")
97
+ end
98
+
99
+ desc "Build and push #{npm_scope}-#{spec.name}-#{spec.version}.tgz to https://npmjs.org"
100
+ task release: %i[build] do
101
+ system("npm publish ./pkg/#{npm_scope}-#{spec.name}-#{spec.version}.tgz --access public")
102
+ end
44
103
  end
104
+
105
+ desc 'Build packages and push them to their respective repository'
106
+ task releases: [:release, 'npm:release']
@@ -2,6 +2,7 @@
2
2
  //= require 'cocooned'
3
3
 
4
4
  // Compatibility with the original Cocoon
5
+ // TODO: Remove in 2.0
5
6
  function initCocoon () {
6
7
  $(Cocooned.prototype.selector('add')).each(function (_i, addLink) {
7
8
  var container = Cocooned.prototype.findContainer(addLink);
@@ -1,3 +1,449 @@
1
- //= require './cocooned/core'
2
- //= require_tree './cocooned/plugins'
3
- //= require_tree './cocooned/jquery'
1
+ /* globals define */
2
+
3
+ (function (root, factory) {
4
+ if (typeof define === 'function' && define.amd) {
5
+ // AMD. Register as an anonymous module.
6
+ define(['jquery'], function (jquery) {
7
+ return (root.Cocooned = factory(jquery));
8
+ });
9
+ } else if (typeof module === 'object' && module.exports) {
10
+ // Node. Does not work with strict CommonJS, but
11
+ // only CommonJS-like environments that support module.exports,
12
+ // like Node.
13
+ module.exports = factory(require('jquery'));
14
+ } else {
15
+ // Browser globals
16
+ root.Cocooned = factory(root.jQuery);
17
+ }
18
+ }(typeof self !== 'undefined' ? self : this, function ($) {
19
+ var Cocooned = function (container, options) {
20
+ this.container = $(container);
21
+ this.options = $.extend({}, this.defaultOptions(), (options || {}));
22
+
23
+ // Autoload plugins
24
+ for (var moduleName in Cocooned.Plugins) {
25
+ if (Cocooned.Plugins.hasOwnProperty(moduleName)) {
26
+ var module = Cocooned.Plugins[moduleName];
27
+ var optionName = moduleName.charAt(0).toLowerCase() + moduleName.slice(1);
28
+
29
+ if (this.options[optionName]) {
30
+ for (var method in module) {
31
+ if (module.hasOwnProperty(method) && typeof module[method] === 'function') {
32
+ this[method] = module[method];
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ this.init();
40
+ };
41
+
42
+ Cocooned.Plugins = {};
43
+ Cocooned.prototype = {
44
+
45
+ elementsCounter: 0,
46
+
47
+ // Compatibility with Cocoon
48
+ // TODO: Remove in 2.0 (Only Cocoon namespaces).
49
+ namespaces: {
50
+ events: ['cocooned', 'cocoon']
51
+ },
52
+
53
+ // Compatibility with Cocoon
54
+ // TODO: Remove in 2.0 (Only Cocoon class names).
55
+ classes: {
56
+ // Actions link
57
+ add: ['cocooned-add', 'add_fields'],
58
+ remove: ['cocooned-remove', 'remove_fields'],
59
+ up: ['cocooned-move-up'],
60
+ down: ['cocooned-move-down'],
61
+ // Containers
62
+ container: ['cocooned-container'],
63
+ item: ['cocooned-item', 'nested-fields']
64
+ },
65
+
66
+ defaultOptions: function () {
67
+ var options = {};
68
+
69
+ for (var moduleName in Cocooned.Plugins) {
70
+ if (Cocooned.Plugins.hasOwnProperty(moduleName)) {
71
+ var module = Cocooned.Plugins[moduleName];
72
+ var optionName = moduleName.charAt(0).toLowerCase() + moduleName.slice(1);
73
+
74
+ options[optionName] = module.defaultOptionValue;
75
+ }
76
+ }
77
+
78
+ return options;
79
+ },
80
+
81
+ notify: function (node, eventType, eventData) {
82
+ return !(this.namespaces.events.some(function (namespace) {
83
+ var namespacedEventType = [namespace, eventType].join(':');
84
+ var event = $.Event(namespacedEventType, eventData);
85
+
86
+ node.trigger(event, [eventData.node, eventData.cocooned]);
87
+
88
+ return (event.isPropagationStopped() || event.isDefaultPrevented());
89
+ }));
90
+ },
91
+
92
+ selector: function (type, selector) {
93
+ var s = selector || '&';
94
+ return this.classes[type].map(function (klass) { return s.replace(/&/, '.' + klass); }).join(', ');
95
+ },
96
+
97
+ namespacedNativeEvents: function (type) {
98
+ var namespaces = this.namespaces.events.map(function (ns) { return '.' + ns; });
99
+ namespaces.unshift(type);
100
+ return namespaces.join('');
101
+ },
102
+
103
+ buildId: function () {
104
+ return (new Date().getTime() + this.elementsCounter++);
105
+ },
106
+
107
+ buildContentNode: function (content) {
108
+ var id = this.buildId();
109
+ var html = (content || this.content);
110
+ var braced = '[' + id + ']';
111
+ var underscored = '_' + id + '_';
112
+
113
+ ['associations', 'association'].forEach(function (a) {
114
+ html = html.replace(this.regexps[a]['braced'], braced + '$1');
115
+ html = html.replace(this.regexps[a]['underscored'], underscored + '$1');
116
+ }, this);
117
+
118
+ return $(html);
119
+ },
120
+
121
+ getInsertionNode: function (adder) {
122
+ var $adder = $(adder);
123
+ var insertionNode = $adder.data('association-insertion-node');
124
+ var insertionTraversal = $adder.data('association-insertion-traversal');
125
+
126
+ if (!insertionNode) {
127
+ return $adder.parent();
128
+ }
129
+
130
+ if (typeof insertionNode === 'function') {
131
+ return insertionNode($adder);
132
+ }
133
+
134
+ if (insertionTraversal) {
135
+ return $adder[insertionTraversal](insertionNode);
136
+ }
137
+
138
+ return insertionNode === 'this' ? $adder : $(insertionNode);
139
+ },
140
+
141
+ getInsertionMethod: function (adder) {
142
+ var $adder = $(adder);
143
+ return $adder.data('association-insertion-method') || 'before';
144
+ },
145
+
146
+ getItems: function (selector) {
147
+ selector = selector || '';
148
+ var self = this;
149
+ return $(this.selector('item', selector), this.container).filter(function () {
150
+ return ($(this).closest(self.selector('container')).get(0) === self.container.get(0));
151
+ });
152
+ },
153
+
154
+ findContainer: function (addLink) {
155
+ var $adder = $(addLink);
156
+ var insertionNode = this.getInsertionNode($adder);
157
+ var insertionMethod = this.getInsertionMethod($adder);
158
+
159
+ switch (insertionMethod) {
160
+ case 'before':
161
+ case 'after':
162
+ case 'replaceWith':
163
+ return insertionNode.parent();
164
+
165
+ case 'append':
166
+ case 'prepend':
167
+ default:
168
+ return insertionNode;
169
+ }
170
+ },
171
+
172
+ findItem: function (removeLink) {
173
+ return $(removeLink).closest(this.selector('item'));
174
+ },
175
+
176
+ init: function () {
177
+ var self = this;
178
+
179
+ this.addLinks = $(this.selector('add')).filter(function () {
180
+ var container = self.findContainer(this);
181
+ return (container.get(0) === self.container.get(0));
182
+ });
183
+
184
+ var addLink = $(this.addLinks.get(0));
185
+
186
+ this.content = addLink.data('association-insertion-template');
187
+ this.regexps = {
188
+ association: {
189
+ braced: new RegExp('\\[new_' + addLink.data('association') + '\\](.*?\\s)', 'g'),
190
+ underscored: new RegExp('_new_' + addLink.data('association') + '_(\\w*)', 'g')
191
+ },
192
+ associations: {
193
+ braced: new RegExp('\\[new_' + addLink.data('associations') + '\\](.*?\\s)', 'g'),
194
+ underscored: new RegExp('_new_' + addLink.data('associations') + '_(\\w*)', 'g')
195
+ }
196
+ };
197
+
198
+ this.initUi();
199
+ this.bindEvents();
200
+ },
201
+
202
+ initUi: function () {
203
+ var self = this;
204
+
205
+ if (!this.container.attr('id')) {
206
+ this.container.attr('id', this.buildId());
207
+ }
208
+ this.container.addClass(this.classes['container'].join(' '));
209
+
210
+ $(function () { self.hideMarkedForDestruction(); });
211
+ $(document).on('page:load turbolinks:load', function () { self.hideMarkedForDestruction(); });
212
+ },
213
+
214
+ bindEvents: function () {
215
+ var self = this;
216
+
217
+ // Bind add links
218
+ this.addLinks.on(
219
+ this.namespacedNativeEvents('click'),
220
+ function (e) {
221
+ e.preventDefault();
222
+ self.add(this);
223
+ });
224
+
225
+ // Bind remove links
226
+ // (Binded on document instead of container to not bypass click handler defined in jquery_ujs)
227
+ $(document).on(
228
+ this.namespacedNativeEvents('click'),
229
+ this.selector('remove', '#' + this.container.attr('id') + ' &'),
230
+ function (e) {
231
+ e.preventDefault();
232
+ self.remove(this);
233
+ });
234
+
235
+ // Bind options events
236
+ $.each(this.options, function (name, value) {
237
+ var bindMethod = 'bind' + name.charAt(0).toUpperCase() + name.slice(1);
238
+ if (value && self[bindMethod]) {
239
+ self[bindMethod]();
240
+ }
241
+ });
242
+ },
243
+
244
+ add: function (adder) {
245
+ var $adder = $(adder);
246
+ var insertionMethod = this.getInsertionMethod($adder);
247
+ var insertionNode = this.getInsertionNode($adder);
248
+ var contentTemplate = $adder.data('association-insertion-template');
249
+ var count = parseInt($adder.data('count'), 10) || 1;
250
+
251
+ for (var i = 0; i < count; i++) {
252
+ var contentNode = this.buildContentNode(contentTemplate);
253
+ var eventData = { link: $adder, node: contentNode, cocooned: this };
254
+ var afterNode = (insertionMethod === 'replaceWith' ? contentNode : insertionNode);
255
+
256
+ // Insertion can be prevented through a 'cocooned:before-insert' event handler
257
+ if (!this.notify(insertionNode, 'before-insert', eventData)) {
258
+ return false;
259
+ }
260
+
261
+ insertionNode[insertionMethod](contentNode);
262
+
263
+ this.notify(afterNode, 'after-insert', eventData);
264
+ }
265
+ },
266
+
267
+ remove: function (remover) {
268
+ var self = this;
269
+ var $remover = $(remover);
270
+ var nodeToDelete = this.findItem($remover);
271
+ var triggerNode = nodeToDelete.parent();
272
+ var eventData = { link: $remover, node: nodeToDelete, cocooned: this };
273
+
274
+ // Deletion can be prevented through a 'cocooned:before-remove' event handler
275
+ if (!this.notify(triggerNode, 'before-remove', eventData)) {
276
+ return false;
277
+ }
278
+
279
+ var timeout = triggerNode.data('remove-timeout') || 0;
280
+
281
+ setTimeout(function () {
282
+ if ($remover.hasClass('dynamic')) {
283
+ nodeToDelete.remove();
284
+ } else {
285
+ nodeToDelete.find('input[required], select[required]').each(function (index, element) {
286
+ $(element).removeAttr('required');
287
+ });
288
+ $remover.siblings('input[type=hidden][name$="[_destroy]"]').val('true');
289
+ nodeToDelete.hide();
290
+ }
291
+ self.notify(triggerNode, 'after-remove', eventData);
292
+ }, timeout);
293
+ },
294
+
295
+ hideMarkedForDestruction: function () {
296
+ var self = this;
297
+ $(this.selector('remove', '&.existing.destroyed'), this.container).each(function (i, removeLink) {
298
+ var node = self.findItem(removeLink);
299
+ node.hide();
300
+ });
301
+ }
302
+ };
303
+
304
+ Cocooned.Plugins.Limit = {
305
+
306
+ defaultOptionValue: false,
307
+
308
+ bindLimit: function () {
309
+ this.limit = this.options['limit'];
310
+ this.container.on('cocooned:before-insert', function (e) {
311
+ var cocooned = e.cocooned;
312
+ if (cocooned.getLength() < cocooned.limit) {
313
+ return;
314
+ }
315
+
316
+ e.stopPropagation();
317
+ var eventData = { link: e.link, node: e.node, cocooned: cocooned };
318
+ cocooned.notify(cocooned.container, 'limit-reached', eventData);
319
+ });
320
+ },
321
+
322
+ getLength: function () {
323
+ return this.getItems('&:visible').length;
324
+ }
325
+ };
326
+
327
+ Cocooned.Plugins.Reorderable = {
328
+
329
+ defaultOptionValue: true,
330
+
331
+ bindReorderable: function () {
332
+ var self = this;
333
+
334
+ // Maintain indexes
335
+ this.container
336
+ .on('cocooned:after-insert', function (e) { self.reindex(); })
337
+ .on('cocooned:after-remove', function (e) { self.reindex(); })
338
+ .on('cocooned:after-move', function (e) { self.reindex(); });
339
+
340
+ // Move items
341
+ this.container.on(
342
+ this.namespacedNativeEvents('click'),
343
+ [this.selector('up'), this.selector('down')].join(', '),
344
+ function (e) {
345
+ e.preventDefault();
346
+ var node = this;
347
+ var up = self.classes['up'].some(function (klass) {
348
+ return node.className.indexOf(klass) !== -1;
349
+ });
350
+ self.move(this, up ? 'up' : 'down');
351
+ });
352
+
353
+ // Ensure positions are unique before save
354
+ this.container.closest('form').on(
355
+ this.namespacedNativeEvents('submit'),
356
+ function (e) {
357
+ self.reindex();
358
+ });
359
+ },
360
+
361
+ move: function (moveLink, direction) {
362
+ var self = this;
363
+ var $mover = $(moveLink);
364
+ var node = $mover.closest(this.selector('item'));
365
+ var siblings = (direction === 'up'
366
+ ? node.prevAll(this.selector('item', '&:eq(0)'))
367
+ : node.nextAll(this.selector('item', '&:eq(0)')));
368
+
369
+ if (siblings.length === 0) {
370
+ return;
371
+ }
372
+
373
+ // Move can be prevented through a 'cocooned:before-move' event handler
374
+ var eventData = { link: $mover, node: node, cocooned: this };
375
+ if (!self.notify(node, 'before-move', eventData)) {
376
+ return false;
377
+ }
378
+
379
+ var height = self.container.outerHeight();
380
+ var width = self.container.outerWidth();
381
+
382
+ self.container.css('height', height).css('width', width);
383
+ self.hide(node, function () {
384
+ var movedNode = $(this).detach();
385
+ movedNode[(direction === 'up' ? 'insertBefore' : 'insertAfter')](siblings);
386
+
387
+ self.show(movedNode, function () {
388
+ self.container.css('height', '').css('width', ''); // Object notation does not work here.
389
+ self.notify(movedNode, 'after-move', eventData);
390
+ });
391
+ });
392
+ },
393
+
394
+ reindex: function () {
395
+ var i = 0;
396
+ var nodes = this.getItems('&:visible');
397
+ var eventData = { link: null, nodes: nodes, cocooned: this };
398
+
399
+ // Reindex can be prevented through a 'cocooned:before-reindex' event handler
400
+ if (!this.notify(this.container, 'before-reindex', eventData)) {
401
+ return false;
402
+ }
403
+
404
+ nodes.each(function () { $('input[id$=_position]', this).val(++i); });
405
+ this.notify(this.container, 'after-reindex', eventData);
406
+ },
407
+
408
+ show: function (node, callback) {
409
+ callback = callback || function () { return true; };
410
+
411
+ node.addClass('cocooned-visible-item');
412
+ setTimeout(function () {
413
+ callback.apply(node);
414
+ node.removeClass('cocooned-hidden-item');
415
+ }, 500);
416
+ },
417
+
418
+ hide: function (node, callback) {
419
+ node.removeClass('cocooned-visible-item').addClass('cocooned-hidden-item');
420
+ if (callback) {
421
+ setTimeout(function () {
422
+ callback.apply(node);
423
+ }, 500);
424
+ }
425
+ }
426
+ };
427
+
428
+ // Expose a jQuery plugin
429
+ $.fn.cocooned = function (options) {
430
+ return this.each(function () {
431
+ var container = $(this);
432
+ if (typeof container.data('cocooned') !== 'undefined') {
433
+ return;
434
+ }
435
+
436
+ var cocooned = new Cocooned(container, options);
437
+ container.data('cocooned', cocooned);
438
+ });
439
+ };
440
+
441
+ // On-load initialization
442
+ $(function () {
443
+ $('*[data-cocooned-options]').each(function (i, el) {
444
+ $(el).cocooned($(el).data('cocooned-options'));
445
+ });
446
+ });
447
+
448
+ return Cocooned;
449
+ }));