cocooned 1.3.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
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
+ }));