cocooned 1.4.1 → 2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +178 -0
- data/README.md +244 -172
- data/app/assets/javascripts/cocoon.js +1 -13
- data/app/assets/javascripts/cocooned.js +929 -399
- data/app/assets/stylesheets/cocooned.css +1 -9
- data/cocooned.gemspec +27 -18
- data/lib/cocooned/association/builder.rb +66 -0
- data/lib/cocooned/association/renderer.rb +53 -0
- data/lib/cocooned/association.rb +8 -0
- data/lib/cocooned/deprecation.rb +105 -0
- data/lib/cocooned/helpers/containers.rb +72 -0
- data/lib/cocooned/helpers/tags/add.rb +136 -0
- data/lib/cocooned/helpers/tags/down.rb +76 -0
- data/lib/cocooned/helpers/tags/remove.rb +78 -0
- data/lib/cocooned/helpers/tags/up.rb +76 -0
- data/lib/cocooned/helpers/tags.rb +60 -0
- data/lib/cocooned/helpers.rb +3 -329
- data/lib/cocooned/railtie.rb +7 -2
- data/lib/cocooned/tags/add.rb +61 -0
- data/lib/cocooned/tags/base.rb +61 -0
- data/lib/cocooned/tags/down.rb +19 -0
- data/lib/cocooned/tags/remove.rb +35 -0
- data/lib/cocooned/tags/up.rb +19 -0
- data/lib/cocooned/tags.rb +12 -0
- data/lib/cocooned/tags_helper.rb +83 -0
- data/lib/cocooned/version.rb +1 -1
- data/lib/cocooned.rb +6 -1
- metadata +51 -86
- data/History.md +0 -283
- data/Rakefile +0 -113
- data/lib/cocooned/association_builder.rb +0 -68
- data/lib/cocooned/helpers/cocoon_compatibility.rb +0 -27
- data/lib/cocooned/helpers/deprecate.rb +0 -47
@@ -1,480 +1,1010 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
(function (
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
1
|
+
(function (global, factory) {
|
2
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
3
|
+
typeof define === 'function' && define.amd ? define(factory) :
|
4
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Cocooned = factory());
|
5
|
+
})(this, (function () { 'use strict';
|
6
|
+
|
7
|
+
class Emitter {
|
8
|
+
constructor (namespaces = ['cocooned']) {
|
9
|
+
this.#namespaces = namespaces;
|
10
|
+
}
|
11
|
+
|
12
|
+
emit (target, type, detail = {}) {
|
13
|
+
return !this.#emitted(target, type, detail).some(e => e.defaultPrevented)
|
14
|
+
}
|
15
|
+
|
16
|
+
/* Protected and private attributes and methods */
|
17
|
+
#namespaces
|
18
|
+
|
19
|
+
#emitted (target, type, detail = {}) {
|
20
|
+
const events = this.#events(type, detail);
|
21
|
+
events.forEach(e => this.#dispatch(target, e));
|
22
|
+
|
23
|
+
return events
|
24
|
+
}
|
25
|
+
|
26
|
+
#dispatch (target, event) {
|
27
|
+
return target.dispatchEvent(event)
|
28
|
+
}
|
29
|
+
|
30
|
+
#events (type, detail) {
|
31
|
+
return this.#namespaces.map(ns => this.#event(`${ns}:${type}`, detail))
|
32
|
+
}
|
33
|
+
|
34
|
+
#event (type, detail) {
|
35
|
+
return new CustomEvent(type, { bubbles: true, cancelable: true, detail })
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
// Borrowed from <https://stackoverflow.com/a/2117523>
|
40
|
+
function uuidv4 () {
|
41
|
+
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
|
42
|
+
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
43
|
+
)
|
44
|
+
}
|
45
|
+
|
46
|
+
function hideMarkedForDestruction (cocooned, items) {
|
47
|
+
items.forEach(item => {
|
48
|
+
const destroy = item.querySelector('input[type=hidden][name$="[_destroy]"]');
|
49
|
+
if (destroy === null) {
|
50
|
+
return
|
51
|
+
}
|
52
|
+
if (destroy.getAttribute('value') !== 'true') {
|
53
|
+
return
|
54
|
+
}
|
55
|
+
|
56
|
+
cocooned.hide(item, { animate: false });
|
10
57
|
});
|
11
|
-
} else if (typeof module === 'object' && module.exports) {
|
12
|
-
// Node. Does not work with strict CommonJS, but
|
13
|
-
// only CommonJS-like environments that support module.exports,
|
14
|
-
// like Node.
|
15
|
-
module.exports = factory(require('jquery'));
|
16
|
-
} else {
|
17
|
-
// Browser globals
|
18
|
-
root.Cocooned = factory(root.jQuery);
|
19
58
|
}
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
}
|
43
|
-
|
44
|
-
this[method] = plugin[method];
|
45
|
-
}
|
46
|
-
}
|
59
|
+
|
60
|
+
function defaultAnimator (item, fetch = false) {
|
61
|
+
if (fetch) {
|
62
|
+
item.dataset.cocoonedScrollHeight = item.scrollHeight;
|
63
|
+
}
|
64
|
+
|
65
|
+
return [
|
66
|
+
{ height: `${item.dataset.cocoonedScrollHeight}px`, opacity: 1 },
|
67
|
+
{ height: `${item.dataset.cocoonedScrollHeight}px`, opacity: 0 },
|
68
|
+
{ height: 0, opacity: 0 }
|
69
|
+
]
|
70
|
+
}
|
71
|
+
|
72
|
+
const instances = Object.create(null);
|
73
|
+
|
74
|
+
class Base {
|
75
|
+
static get defaultOptions () {
|
76
|
+
const element = document.createElement('div');
|
77
|
+
return {
|
78
|
+
animate: ('animate' in element && typeof element.animate === 'function'),
|
79
|
+
animator: defaultAnimator,
|
80
|
+
duration: 450
|
47
81
|
}
|
48
82
|
}
|
49
83
|
|
50
|
-
|
51
|
-
|
52
|
-
|
84
|
+
static get eventNamespaces () {
|
85
|
+
return ['cocooned']
|
86
|
+
}
|
53
87
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
// Compatibility with Cocoon
|
60
|
-
// TODO: Remove in 3.0 (Only Cocoon namespaces).
|
61
|
-
namespaces: {
|
62
|
-
events: ['cocooned', 'cocoon']
|
63
|
-
},
|
64
|
-
|
65
|
-
// Compatibility with Cocoon
|
66
|
-
// TODO: Remove in 3.0 (Only Cocoon class names).
|
67
|
-
classes: {
|
68
|
-
// Actions link
|
69
|
-
add: ['cocooned-add', 'add_fields'],
|
70
|
-
remove: ['cocooned-remove', 'remove_fields'],
|
71
|
-
up: ['cocooned-move-up'],
|
72
|
-
down: ['cocooned-move-down'],
|
73
|
-
// Containers
|
74
|
-
container: ['cocooned-container'],
|
75
|
-
item: ['cocooned-item', 'nested-fields']
|
76
|
-
},
|
77
|
-
|
78
|
-
defaultOptions: function () {
|
79
|
-
var options = {};
|
80
|
-
|
81
|
-
for (var moduleName in Cocooned.Plugins) {
|
82
|
-
if (Cocooned.Plugins.hasOwnProperty(moduleName)) {
|
83
|
-
var module = Cocooned.Plugins[moduleName];
|
84
|
-
var optionName = moduleName.charAt(0).toLowerCase() + moduleName.slice(1);
|
85
|
-
|
86
|
-
options[optionName] = module.defaultOptionValue;
|
87
|
-
}
|
88
|
+
static get selectors () {
|
89
|
+
return {
|
90
|
+
container: ['[data-cocooned-container]', '.cocooned-container'],
|
91
|
+
item: ['[data-cocooned-item]', '.cocooned-item']
|
88
92
|
}
|
93
|
+
}
|
89
94
|
|
90
|
-
|
91
|
-
|
95
|
+
static getInstance (uuid) {
|
96
|
+
return instances[uuid]
|
97
|
+
}
|
92
98
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
99
|
+
constructor (container, options) {
|
100
|
+
this._container = container;
|
101
|
+
this._uuid = uuidv4();
|
102
|
+
this._options = this.constructor._normalizeOptions({
|
103
|
+
...this.constructor.defaultOptions,
|
104
|
+
...('cocoonedOptions' in container.dataset ? JSON.parse(container.dataset.cocoonedOptions) : {}),
|
105
|
+
...(options || {})
|
106
|
+
});
|
107
|
+
}
|
97
108
|
|
98
|
-
|
109
|
+
get container () {
|
110
|
+
return this._container
|
111
|
+
}
|
99
112
|
|
100
|
-
|
101
|
-
|
102
|
-
}
|
113
|
+
get options () {
|
114
|
+
return this._options
|
115
|
+
}
|
103
116
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
117
|
+
start () {
|
118
|
+
this.container.dataset.cocoonedContainer = true;
|
119
|
+
this.container.dataset.cocoonedUuid = this._uuid;
|
120
|
+
instances[this._uuid] = this;
|
108
121
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
122
|
+
const hideDestroyed = () => { hideMarkedForDestruction(this, this.items); };
|
123
|
+
|
124
|
+
hideDestroyed();
|
125
|
+
this.container.ownerDocument.addEventListener('page:load', hideDestroyed);
|
126
|
+
this.container.ownerDocument.addEventListener('turbo:load', hideDestroyed);
|
127
|
+
this.container.ownerDocument.addEventListener('turbolinks:load', hideDestroyed);
|
128
|
+
}
|
114
129
|
|
115
|
-
|
116
|
-
return
|
117
|
-
}
|
130
|
+
notify (node, eventType, eventData) {
|
131
|
+
return this._emitter.emit(node, eventType, eventData)
|
132
|
+
}
|
118
133
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
134
|
+
/* Selections methods */
|
135
|
+
get items () {
|
136
|
+
return Array.from(this.container.querySelectorAll(this._selector('item')))
|
137
|
+
.filter(item => this.toContainer(item) === this.container)
|
138
|
+
.filter(item => !('display' in item.style && item.style.display === 'none'))
|
139
|
+
}
|
124
140
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
}, this);
|
141
|
+
toContainer (node) {
|
142
|
+
return node.closest(this._selector('container'))
|
143
|
+
}
|
129
144
|
|
130
|
-
|
131
|
-
|
145
|
+
toItem (node) {
|
146
|
+
return node.closest(this._selector('item'))
|
147
|
+
}
|
132
148
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
var insertionTraversal = $adder.data('association-insertion-traversal');
|
149
|
+
contains (node) {
|
150
|
+
return this.items.includes(this.toItem(node))
|
151
|
+
}
|
137
152
|
|
138
|
-
|
139
|
-
|
153
|
+
hide (item, options = {}) {
|
154
|
+
const opts = this._animationOptions(options);
|
155
|
+
const keyframes = opts.animator(item, true);
|
156
|
+
const after = () => { item.style.display = 'none'; };
|
157
|
+
|
158
|
+
if (!opts.animate) {
|
159
|
+
return Promise.resolve(after()).then(() => item)
|
140
160
|
}
|
161
|
+
return item.animate(keyframes, opts.duration).finished.then(after).then(() => item)
|
162
|
+
}
|
163
|
+
|
164
|
+
show (item, options = {}) {
|
165
|
+
const opts = this._animationOptions(options);
|
166
|
+
const keyframes = opts.animator(item, false).reverse();
|
167
|
+
const before = () => { item.style.display = null; };
|
141
168
|
|
142
|
-
|
143
|
-
|
169
|
+
const promise = Promise.resolve(before());
|
170
|
+
if (!opts.animate) {
|
171
|
+
return promise.then(() => item)
|
144
172
|
}
|
173
|
+
return promise.then(() => item.animate(keyframes, opts.duration).finished).then(() => item)
|
174
|
+
}
|
175
|
+
|
176
|
+
/* Protected and private attributes and methods */
|
177
|
+
static _normalizeOptions (options) {
|
178
|
+
return options
|
179
|
+
}
|
145
180
|
|
146
|
-
|
147
|
-
|
181
|
+
_container
|
182
|
+
_options
|
183
|
+
__uuid
|
184
|
+
__emitter
|
185
|
+
|
186
|
+
get _emitter () {
|
187
|
+
if (typeof this.__emitter === 'undefined') {
|
188
|
+
this.__emitter = new Emitter(this.constructor.eventNamespaces);
|
148
189
|
}
|
149
190
|
|
150
|
-
return
|
151
|
-
}
|
191
|
+
return this.__emitter
|
192
|
+
}
|
152
193
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
},
|
194
|
+
_selectors (name) {
|
195
|
+
return this.constructor.selectors[name]
|
196
|
+
}
|
157
197
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
});
|
164
|
-
|
198
|
+
_selector (name) {
|
199
|
+
return this._selectors(name).join(', ')
|
200
|
+
}
|
201
|
+
|
202
|
+
_animationOptions (options) {
|
203
|
+
const defaults = (({ animate, animator, duration }) => ({ animate, animator, duration }))(this._options);
|
204
|
+
return { ...defaults, ...options }
|
205
|
+
}
|
206
|
+
}
|
207
|
+
|
208
|
+
class Trigger {
|
209
|
+
constructor (trigger, cocooned) {
|
210
|
+
this._trigger = trigger;
|
211
|
+
this._cocooned = cocooned;
|
212
|
+
}
|
213
|
+
|
214
|
+
get trigger () {
|
215
|
+
return this._trigger
|
216
|
+
}
|
217
|
+
|
218
|
+
handle (event) {
|
219
|
+
throw new TypeError('handle() must be defined in subclasses')
|
220
|
+
}
|
221
|
+
|
222
|
+
/* Protected and private attributes and methods */
|
223
|
+
_cocooned
|
224
|
+
_trigger
|
165
225
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
226
|
+
get _item () {
|
227
|
+
return this._cocooned.toItem(this._trigger)
|
228
|
+
}
|
229
|
+
|
230
|
+
get _notified () {
|
231
|
+
return this._item
|
232
|
+
}
|
170
233
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
234
|
+
_notify (eventName, originalEvent) {
|
235
|
+
return this._cocooned.notify(this._notified, eventName, this._eventData(originalEvent))
|
236
|
+
}
|
237
|
+
|
238
|
+
_eventData (originalEvent) {
|
239
|
+
return { link: this._trigger, node: this._item, cocooned: this._cocooned, originalEvent }
|
240
|
+
}
|
241
|
+
|
242
|
+
_hide (node, callback) {
|
243
|
+
return this._cocooned.hide(node, callback)
|
244
|
+
}
|
245
|
+
|
246
|
+
_show (node, callback) {
|
247
|
+
return this._cocooned.show(node, callback)
|
248
|
+
}
|
249
|
+
}
|
176
250
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
251
|
+
/**
|
252
|
+
* Borrowed from Lodash
|
253
|
+
* See https://lodash.com/docs/#escapeRegExp
|
254
|
+
*/
|
255
|
+
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
|
256
|
+
const reHasRegExpChar = RegExp(reRegExpChar.source);
|
257
|
+
|
258
|
+
class Replacement {
|
259
|
+
attribute
|
260
|
+
|
261
|
+
constructor (attribute, name, startDelimiter, endDelimiter = null) {
|
262
|
+
this.attribute = attribute;
|
263
|
+
|
264
|
+
this.#name = name;
|
265
|
+
this.#startDelimiter = startDelimiter;
|
266
|
+
this.#endDelimiter = endDelimiter || startDelimiter;
|
267
|
+
}
|
268
|
+
|
269
|
+
apply (node, id) {
|
270
|
+
const value = node.getAttribute(this.attribute);
|
271
|
+
if (!this.#regexp.test(value)) {
|
272
|
+
return
|
181
273
|
}
|
182
|
-
},
|
183
274
|
|
184
|
-
|
185
|
-
|
186
|
-
|
275
|
+
node.setAttribute(this.attribute, value.replace(this.#regexp, this.#replacement(id)));
|
276
|
+
}
|
277
|
+
|
278
|
+
/* Protected and private attributes and methods */
|
279
|
+
#name
|
280
|
+
#startDelimiter
|
281
|
+
#endDelimiter
|
187
282
|
|
188
|
-
|
189
|
-
|
283
|
+
#replacement (id) {
|
284
|
+
return `${this.#startDelimiter}${id}${this.#endDelimiter}$1`
|
285
|
+
}
|
286
|
+
|
287
|
+
get #regexp () {
|
288
|
+
const escaped = this.#escape(`${this.#startDelimiter}${this.#name}${this.#endDelimiter}`);
|
289
|
+
return new RegExp(`${escaped}(.*?)`, 'g')
|
290
|
+
}
|
190
291
|
|
191
|
-
|
192
|
-
|
193
|
-
|
292
|
+
#escape (string) {
|
293
|
+
return (string && reHasRegExpChar.test(string))
|
294
|
+
? string.replace(reRegExpChar, '\\$&')
|
295
|
+
: (string || '')
|
296
|
+
}
|
297
|
+
}
|
298
|
+
|
299
|
+
class Builder {
|
300
|
+
constructor (documentFragment, association) {
|
301
|
+
this.#documentFragment = documentFragment;
|
302
|
+
this.#association = association;
|
303
|
+
this.#replacements = [
|
304
|
+
new Replacement('for', association, '_'),
|
305
|
+
new Replacement('id', association, '_'),
|
306
|
+
new Replacement('name', association, '[', ']')
|
307
|
+
];
|
308
|
+
}
|
309
|
+
|
310
|
+
build (id) {
|
311
|
+
const node = this.#documentFragment.cloneNode(true);
|
312
|
+
this.#replacements.forEach(replacement => {
|
313
|
+
node.querySelectorAll(`*[${replacement.attribute}]`).forEach(node => replacement.apply(node, id));
|
194
314
|
});
|
195
315
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
this
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
});
|
238
|
-
|
239
|
-
// Bind remove links
|
240
|
-
// (Binded on document instead of container to not bypass click handler defined in jquery_ujs)
|
241
|
-
$(document).on(
|
242
|
-
this.namespacedNativeEvents('click'),
|
243
|
-
this.selector('remove', '#' + this.container.attr('id') + ' &'),
|
244
|
-
function (e) {
|
245
|
-
e.preventDefault();
|
246
|
-
e.stopPropagation();
|
247
|
-
|
248
|
-
self.remove(this, e);
|
249
|
-
});
|
250
|
-
|
251
|
-
// Bind options events
|
252
|
-
$.each(this.options, function (name, value) {
|
253
|
-
var bindMethod = 'bind' + name.charAt(0).toUpperCase() + name.slice(1);
|
254
|
-
if (value && self[bindMethod]) {
|
255
|
-
self[bindMethod]();
|
316
|
+
return node
|
317
|
+
}
|
318
|
+
|
319
|
+
/* Protected and private attributes and methods */
|
320
|
+
#association
|
321
|
+
#documentFragment
|
322
|
+
#replacements
|
323
|
+
}
|
324
|
+
|
325
|
+
class Traverser {
|
326
|
+
constructor (origin, traversal) {
|
327
|
+
this.#origin = origin;
|
328
|
+
this.#traversal = traversal;
|
329
|
+
}
|
330
|
+
|
331
|
+
resolve (selector) {
|
332
|
+
if (this.#traversal in this.#origin && typeof this.#origin[this.#traversal] === 'function') {
|
333
|
+
return this._tryMethod(this.#traversal, selector)
|
334
|
+
}
|
335
|
+
|
336
|
+
if (this.#traversal in this.#origin) {
|
337
|
+
return this._tryProperty(this.#traversal)
|
338
|
+
}
|
339
|
+
|
340
|
+
const method = `_${this.#traversal}`;
|
341
|
+
if (method in this) {
|
342
|
+
return this[method](selector)
|
343
|
+
}
|
344
|
+
|
345
|
+
return null
|
346
|
+
}
|
347
|
+
|
348
|
+
/* Protected and private attributes and methods */
|
349
|
+
#origin
|
350
|
+
#traversal
|
351
|
+
|
352
|
+
_tryMethod (method, selector) {
|
353
|
+
try {
|
354
|
+
const resolved = this.#origin[method](selector);
|
355
|
+
if (resolved instanceof HTMLElement) {
|
356
|
+
return resolved
|
256
357
|
}
|
257
|
-
})
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
358
|
+
} catch (e) {}
|
359
|
+
|
360
|
+
return null
|
361
|
+
}
|
362
|
+
|
363
|
+
_tryProperty (property) {
|
364
|
+
const resolved = this.#origin[property];
|
365
|
+
if (resolved instanceof HTMLElement) {
|
366
|
+
return resolved
|
367
|
+
}
|
368
|
+
|
369
|
+
return null
|
370
|
+
}
|
371
|
+
|
372
|
+
_parent (selector) {
|
373
|
+
if (this.#origin.parentElement.matches(selector)) {
|
374
|
+
return this.#origin.parentElement
|
375
|
+
}
|
376
|
+
return null
|
377
|
+
}
|
378
|
+
|
379
|
+
_prev (selector) {
|
380
|
+
if (this.#origin.previousElementSibling.matches(selector)) {
|
381
|
+
return this.#origin.previousElementSibling
|
382
|
+
}
|
383
|
+
return null
|
384
|
+
}
|
385
|
+
|
386
|
+
_next (selector) {
|
387
|
+
if (this.#origin.nextElementSibling.matches(selector)) {
|
388
|
+
return this.#origin.nextElementSibling
|
389
|
+
}
|
390
|
+
return null
|
391
|
+
}
|
392
|
+
|
393
|
+
_siblings (selector) {
|
394
|
+
return this.#origin.parentElement.querySelector(selector)
|
395
|
+
}
|
396
|
+
}
|
397
|
+
|
398
|
+
class Deprecator {
|
399
|
+
logger
|
400
|
+
package
|
401
|
+
version
|
402
|
+
|
403
|
+
constructor (version, packageName, logger) {
|
404
|
+
this.version = version;
|
405
|
+
this.package = packageName;
|
406
|
+
this.logger = logger;
|
407
|
+
}
|
408
|
+
|
409
|
+
warn (message, replacement = null) {
|
410
|
+
if (message in this.#emitted) {
|
411
|
+
return
|
412
|
+
}
|
413
|
+
|
414
|
+
const warning = `${message}. It will be removed from ${this.package} ${this.version}`;
|
415
|
+
const alternative = (replacement !== null ? `, use ${replacement} instead` : '');
|
416
|
+
this.logger.warn(`DEPRECATION WARNING: ${warning}${alternative}.`);
|
417
|
+
|
418
|
+
this.#emitted[message] = true;
|
419
|
+
}
|
420
|
+
|
421
|
+
/* Protected and private attributes and methods */
|
422
|
+
#emitted = Object.create(null)
|
423
|
+
}
|
424
|
+
|
425
|
+
const deprecators = Object.create(null);
|
426
|
+
|
427
|
+
function deprecator (version, packageName = 'Cocooned', logger = console) {
|
428
|
+
const hash = [version, packageName].join('#');
|
429
|
+
if (!(hash in deprecators)) {
|
430
|
+
deprecators[hash] = new Deprecator(version, packageName, logger);
|
431
|
+
}
|
432
|
+
|
433
|
+
return deprecators[hash]
|
434
|
+
}
|
435
|
+
|
436
|
+
class Extractor {
|
437
|
+
constructor (trigger) {
|
438
|
+
this.#trigger = trigger;
|
439
|
+
}
|
440
|
+
|
441
|
+
extract () {
|
442
|
+
return ['builder', 'count', 'node', 'method'].reduce((options, option) => {
|
443
|
+
// Sadly, this does not seem to work with #privateMethods
|
444
|
+
const method = `_extract${option.charAt(0).toUpperCase() + option.slice(1)}`;
|
445
|
+
const extracted = this[method]();
|
446
|
+
if (extracted !== null) {
|
447
|
+
options[option] = extracted;
|
275
448
|
}
|
276
449
|
|
277
|
-
|
450
|
+
return options
|
451
|
+
}, {})
|
452
|
+
}
|
453
|
+
|
454
|
+
/* Protected and private attributes and methods */
|
455
|
+
#trigger
|
456
|
+
|
457
|
+
get #dataset () {
|
458
|
+
return this.#trigger.dataset
|
459
|
+
}
|
460
|
+
|
461
|
+
_extractBuilder () {
|
462
|
+
if (!('template' in this.#dataset && 'association' in this.#dataset)) {
|
463
|
+
return null
|
464
|
+
}
|
465
|
+
|
466
|
+
const template = document.querySelector(`template[data-name="${this.#dataset.template}"]`);
|
467
|
+
if (template === null) {
|
468
|
+
return null
|
469
|
+
}
|
470
|
+
|
471
|
+
return new Builder(template.content, `new_${this.#dataset.association}`)
|
472
|
+
}
|
473
|
+
|
474
|
+
_extractCount () {
|
475
|
+
if ('associationInsertionCount' in this.#dataset) {
|
476
|
+
return parseInt(this.#dataset.associationInsertionCount, 10)
|
477
|
+
}
|
478
|
+
|
479
|
+
if ('count' in this.#dataset) {
|
480
|
+
return parseInt(this.#dataset.count, 10)
|
481
|
+
}
|
482
|
+
|
483
|
+
return null
|
484
|
+
}
|
485
|
+
|
486
|
+
_extractMethod () {
|
487
|
+
if ('associationInsertionMethod' in this.#dataset) {
|
488
|
+
return this.#dataset.associationInsertionMethod
|
489
|
+
}
|
490
|
+
|
491
|
+
return 'before'
|
492
|
+
}
|
493
|
+
|
494
|
+
_extractNode () {
|
495
|
+
if (!('associationInsertionNode' in this.#dataset)) {
|
496
|
+
return this.#trigger.parentElement
|
497
|
+
}
|
498
|
+
|
499
|
+
const node = this.#dataset.associationInsertionNode;
|
500
|
+
if (node === 'this') {
|
501
|
+
return this.#trigger
|
502
|
+
}
|
503
|
+
|
504
|
+
if (!('associationInsertionTraversal' in this.#dataset)) {
|
505
|
+
return this.#trigger.ownerDocument.querySelector(node)
|
506
|
+
}
|
507
|
+
|
508
|
+
deprecator('3.0').warn('associationInsertionTraversal is deprecated');
|
509
|
+
const traverser = new Traverser(this.#trigger, this.#dataset.associationInsertionTraversal);
|
510
|
+
|
511
|
+
return traverser.resolve(node)
|
512
|
+
}
|
513
|
+
}
|
514
|
+
|
515
|
+
class Validator {
|
516
|
+
static validates (options) {
|
517
|
+
const validator = new Validator(options);
|
518
|
+
return validator.validates()
|
519
|
+
}
|
520
|
+
|
521
|
+
constructor (options) {
|
522
|
+
this.#options = options;
|
523
|
+
}
|
524
|
+
|
525
|
+
validates () {
|
526
|
+
const optionNames = new Set(Object.keys(this.#options));
|
527
|
+
const expected = new Set(['builder', 'count', 'node', 'method']);
|
528
|
+
const missing = new Set(Array.from(expected.values()).filter(key => !optionNames.has(key)));
|
529
|
+
|
530
|
+
if (missing.size !== 0) {
|
531
|
+
throw new TypeError(`Missing options: ${Array.from(missing.values()).join(', ')}`)
|
532
|
+
}
|
533
|
+
|
534
|
+
this._validateBuilder();
|
535
|
+
this._validateMethod();
|
536
|
+
}
|
537
|
+
|
538
|
+
/* Protected and private attributes and methods */
|
539
|
+
#options
|
278
540
|
|
279
|
-
|
541
|
+
_validateBuilder () {
|
542
|
+
const builder = this.#options.builder;
|
543
|
+
if (!(builder instanceof Builder)) {
|
544
|
+
throw new TypeError(
|
545
|
+
`Invalid builder option: instance of Builder expected, got ${builder.constructor.name}`
|
546
|
+
)
|
280
547
|
}
|
281
|
-
}
|
548
|
+
}
|
282
549
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
var nodeToDelete = this.findItem($remover);
|
287
|
-
var triggerNode = nodeToDelete.parent();
|
288
|
-
var eventData = { link: $remover, node: nodeToDelete, cocooned: this, originalEvent: originalEvent };
|
550
|
+
_validateMethod () {
|
551
|
+
const method = this.#options.method;
|
552
|
+
const supported = ['after', 'before', 'append', 'prepend', 'replaceWith'];
|
289
553
|
|
290
|
-
|
291
|
-
|
292
|
-
|
554
|
+
if (!supported.includes(method)) {
|
555
|
+
throw new TypeError(
|
556
|
+
`Invalid method option: expected one of ${supported.join(', ')}, got ${method}`
|
557
|
+
)
|
293
558
|
}
|
559
|
+
}
|
560
|
+
}
|
561
|
+
|
562
|
+
let counter = 0;
|
563
|
+
|
564
|
+
function uniqueId () {
|
565
|
+
return `${new Date().getTime()}${counter++}`
|
566
|
+
}
|
567
|
+
|
568
|
+
class Add extends Trigger {
|
569
|
+
static create (trigger, cocooned) {
|
570
|
+
const extractor = new Extractor(trigger);
|
571
|
+
return new Add(trigger, cocooned, extractor.extract())
|
572
|
+
}
|
294
573
|
|
295
|
-
|
574
|
+
constructor (trigger, cocooned, options = {}) {
|
575
|
+
super(trigger, cocooned);
|
296
576
|
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
577
|
+
this.#options = { ...this.#options, ...options };
|
578
|
+
Validator.validates(this.#options);
|
579
|
+
}
|
580
|
+
|
581
|
+
get insertionNode () {
|
582
|
+
return this.#options.node
|
583
|
+
}
|
584
|
+
|
585
|
+
handle (event) {
|
586
|
+
for (let i = 0; i < this.#options.count; i++) {
|
587
|
+
this.#item = this._build();
|
588
|
+
|
589
|
+
// Insert can be prevented through a 'cocooned:before-insert' event handler
|
590
|
+
if (!this._notify('before-insert', event)) {
|
591
|
+
return false
|
306
592
|
}
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
593
|
+
|
594
|
+
this._insert();
|
595
|
+
this._notify('after-insert', event);
|
596
|
+
}
|
597
|
+
}
|
598
|
+
|
599
|
+
/* Protected and private attributes and methods */
|
600
|
+
#item
|
601
|
+
#options = {
|
602
|
+
count: 1
|
603
|
+
// Other expected options:
|
604
|
+
// builder: A Builder instance
|
605
|
+
// method: Insertion method (one of: append, prepend, before, after, replaceWith)
|
606
|
+
// node: Insertion Node as a DOM Element
|
607
|
+
}
|
608
|
+
|
609
|
+
get _item () {
|
610
|
+
return this.#item
|
611
|
+
}
|
612
|
+
|
613
|
+
get _notified () {
|
614
|
+
return this.#options.node
|
615
|
+
}
|
616
|
+
|
617
|
+
_insert () {
|
618
|
+
this.#options.node[this.#options.method](this._item);
|
619
|
+
}
|
620
|
+
|
621
|
+
_build () {
|
622
|
+
return this.#options.builder.build(uniqueId()).firstElementChild
|
623
|
+
}
|
624
|
+
}
|
625
|
+
|
626
|
+
class Remove extends Trigger {
|
627
|
+
handle (event) {
|
628
|
+
// Removal can be prevented through a 'cocooned:before-remove' event handler
|
629
|
+
if (!this._notify('before-remove', event)) {
|
630
|
+
return false
|
631
|
+
}
|
632
|
+
|
633
|
+
this._hide(this._item).then(() => {
|
634
|
+
this._remove();
|
635
|
+
this._notify('after-remove', event);
|
316
636
|
});
|
317
637
|
}
|
638
|
+
|
639
|
+
/* Protected and private attributes and methods */
|
640
|
+
#notified
|
641
|
+
|
642
|
+
// Dynamic nodes are plainly removed from document, so we need to trigger
|
643
|
+
// events on their parent and memoize it so we still can find it after removal
|
644
|
+
get _notified () {
|
645
|
+
if (typeof this.#notified === 'undefined') {
|
646
|
+
this.#notified = this._item.parentElement;
|
647
|
+
}
|
648
|
+
|
649
|
+
return this.#notified
|
650
|
+
}
|
651
|
+
|
652
|
+
_remove () {
|
653
|
+
this._removable() ? this._item.remove() : this._markForDestruction();
|
654
|
+
}
|
655
|
+
|
656
|
+
_removable () {
|
657
|
+
return this._trigger.matches('.dynamic') ||
|
658
|
+
('cocoonedPersisted' in this._trigger.dataset && this._trigger.dataset.cocoonedPersisted === 'false')
|
659
|
+
}
|
660
|
+
|
661
|
+
_markForDestruction () {
|
662
|
+
this._item.querySelector('input[type=hidden][name$="[_destroy]"]').setAttribute('value', 'true');
|
663
|
+
this._item.querySelectorAll('input[required], select[required]')
|
664
|
+
.forEach(input => input.removeAttribute('required'));
|
665
|
+
}
|
666
|
+
}
|
667
|
+
|
668
|
+
function clickHandler$1 (callback) {
|
669
|
+
return e => {
|
670
|
+
e.preventDefault();
|
671
|
+
callback(e);
|
672
|
+
}
|
673
|
+
}
|
674
|
+
|
675
|
+
function delegatedClickHandler (selector, callback) {
|
676
|
+
return e => {
|
677
|
+
const { target } = e;
|
678
|
+
if (!target.matches(selector)) {
|
679
|
+
return
|
680
|
+
}
|
681
|
+
|
682
|
+
e.preventDefault();
|
683
|
+
callback(e);
|
684
|
+
}
|
685
|
+
}
|
686
|
+
|
687
|
+
const coreMixin = (Base) => class extends Base {
|
688
|
+
static get selectors () {
|
689
|
+
return {
|
690
|
+
...super.selectors,
|
691
|
+
'triggers.add': ['[data-cocooned-trigger="add"]', '.cocooned-add'],
|
692
|
+
'triggers.remove': ['[data-cocooned-trigger="remove"]', '.cocooned-remove']
|
693
|
+
}
|
694
|
+
}
|
695
|
+
|
696
|
+
start () {
|
697
|
+
super.start();
|
698
|
+
|
699
|
+
this.addTriggers = Array.from(this.container.ownerDocument.querySelectorAll(this._selector('triggers.add')))
|
700
|
+
.map(element => Add.create(element, this))
|
701
|
+
.filter(trigger => this.toContainer(trigger.insertionNode) === this.container);
|
702
|
+
|
703
|
+
this.addTriggers.forEach(add => add.trigger.addEventListener(
|
704
|
+
'click',
|
705
|
+
clickHandler$1((e) => add.handle(e))
|
706
|
+
));
|
707
|
+
|
708
|
+
this.container.addEventListener(
|
709
|
+
'click',
|
710
|
+
delegatedClickHandler(this._selector('triggers.remove'), (e) => {
|
711
|
+
const trigger = new Remove(e.target, this);
|
712
|
+
trigger.handle(e);
|
713
|
+
})
|
714
|
+
);
|
715
|
+
}
|
318
716
|
};
|
319
717
|
|
320
|
-
Cocooned
|
718
|
+
let Cocooned$1 = class Cocooned extends coreMixin(Base) {
|
719
|
+
static create (container, options) {
|
720
|
+
if ('cocoonedUuid' in container.dataset) {
|
721
|
+
return Cocooned.getInstance(container.dataset.cocoonedUuid)
|
722
|
+
}
|
321
723
|
|
322
|
-
|
724
|
+
const cocooned = new this.constructor(container, options);
|
725
|
+
cocooned.start();
|
726
|
+
|
727
|
+
return cocooned
|
728
|
+
}
|
729
|
+
|
730
|
+
static start () {
|
731
|
+
document.querySelectorAll('[data-cocooned-container], [data-cocooned-options]')
|
732
|
+
.forEach(element => this.constructor.create(element));
|
733
|
+
}
|
734
|
+
};
|
323
735
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
736
|
+
const limitMixin = (Base) => class extends Base {
|
737
|
+
static get defaultOptions () {
|
738
|
+
return { ...super.defaultOptions, ...{ limit: false } }
|
739
|
+
}
|
740
|
+
|
741
|
+
start () {
|
742
|
+
super.start();
|
743
|
+
if (this.options.limit === false) {
|
744
|
+
return
|
745
|
+
}
|
746
|
+
|
747
|
+
this.container.addEventListener('cocooned:before-insert', e => {
|
748
|
+
if (this.items.length < this.options.limit) {
|
749
|
+
return
|
330
750
|
}
|
331
751
|
|
332
|
-
e.
|
333
|
-
|
334
|
-
cocooned.notify(cocooned.container, 'limit-reached', eventData);
|
752
|
+
e.preventDefault();
|
753
|
+
this.notify(this.container, 'limit-reached', e.detail);
|
335
754
|
});
|
336
|
-
},
|
337
|
-
|
338
|
-
getLength: function () {
|
339
|
-
return this.getItems('&:visible').length;
|
340
755
|
}
|
341
756
|
};
|
342
757
|
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
// Maintain indexes
|
359
|
-
this.container
|
360
|
-
.on('cocooned:after-insert', function (e) { self.reindex(e); })
|
361
|
-
.on('cocooned:after-remove', function (e) { self.reindex(e); })
|
362
|
-
.on('cocooned:after-move', function (e) { self.reindex(e); });
|
363
|
-
|
364
|
-
// Move items
|
365
|
-
this.container.on(
|
366
|
-
this.namespacedNativeEvents('click'),
|
367
|
-
[this.selector('up'), this.selector('down')].join(', '),
|
368
|
-
function (e) {
|
369
|
-
e.preventDefault();
|
370
|
-
e.stopPropagation();
|
371
|
-
|
372
|
-
var node = this;
|
373
|
-
var up = self.classes['up'].some(function (klass) {
|
374
|
-
return node.className.indexOf(klass) !== -1;
|
375
|
-
});
|
376
|
-
self.move(this, up ? 'up' : 'down', e);
|
377
|
-
});
|
378
|
-
|
379
|
-
// Ensure positions are unique before save
|
380
|
-
this.container.closest('form').on(
|
381
|
-
this.namespacedNativeEvents('submit'),
|
382
|
-
function (e) {
|
383
|
-
self.reindex(e);
|
384
|
-
});
|
385
|
-
},
|
386
|
-
|
387
|
-
move: function (moveLink, direction, originalEvent) {
|
388
|
-
var self = this;
|
389
|
-
var $mover = $(moveLink);
|
390
|
-
var node = $mover.closest(this.selector('item'));
|
391
|
-
var siblings = (direction === 'up'
|
392
|
-
? node.prevAll(this.selector('item', '&:eq(0)'))
|
393
|
-
: node.nextAll(this.selector('item', '&:eq(0)')));
|
394
|
-
|
395
|
-
if (siblings.length === 0) {
|
396
|
-
return;
|
397
|
-
}
|
398
|
-
|
399
|
-
// Move can be prevented through a 'cocooned:before-move' event handler
|
400
|
-
var eventData = { link: $mover, node: node, cocooned: this, originalEvent: originalEvent };
|
401
|
-
if (!self.notify(node, 'before-move', eventData)) {
|
402
|
-
return false;
|
403
|
-
}
|
404
|
-
|
405
|
-
var height = self.container.outerHeight();
|
406
|
-
var width = self.container.outerWidth();
|
407
|
-
|
408
|
-
self.container.css('height', height).css('width', width);
|
409
|
-
self.hide(node, function () {
|
410
|
-
var movedNode = $(this).detach();
|
411
|
-
movedNode[(direction === 'up' ? 'insertBefore' : 'insertAfter')](siblings);
|
412
|
-
|
413
|
-
self.show(movedNode, function () {
|
414
|
-
self.container.css('height', '').css('width', ''); // Object notation does not work here.
|
415
|
-
self.notify(movedNode, 'after-move', eventData);
|
416
|
-
});
|
758
|
+
class Move extends Trigger {
|
759
|
+
handle (event) {
|
760
|
+
if (this._pivotItem === null) {
|
761
|
+
return
|
762
|
+
}
|
763
|
+
|
764
|
+
// Moves can be prevented through a 'cocooned:before-move' event handler
|
765
|
+
if (!this._notify('before-move', event)) {
|
766
|
+
return false
|
767
|
+
}
|
768
|
+
|
769
|
+
this._hide(this._item).then(() => {
|
770
|
+
this._move();
|
771
|
+
this._show(this._item).then(() => this._notify('after-move', event));
|
417
772
|
});
|
418
|
-
}
|
773
|
+
}
|
419
774
|
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
775
|
+
/* Protected and private attributes and methods */
|
776
|
+
get _pivotItem () {
|
777
|
+
throw new TypeError('_pivotItem() must be defined in subclasses')
|
778
|
+
}
|
779
|
+
|
780
|
+
_move () {
|
781
|
+
throw new TypeError('_move() must be defined in subclasses')
|
782
|
+
}
|
783
|
+
|
784
|
+
_findPivotItem (origin, method) {
|
785
|
+
let sibling = origin;
|
786
|
+
|
787
|
+
do {
|
788
|
+
sibling = sibling[method];
|
789
|
+
if (sibling !== null && this._cocooned.contains(sibling)) {
|
790
|
+
break
|
791
|
+
}
|
792
|
+
} while (sibling !== null)
|
793
|
+
|
794
|
+
return sibling
|
795
|
+
}
|
796
|
+
}
|
797
|
+
|
798
|
+
class Up extends Move {
|
799
|
+
/* Protected and private attributes and methods */
|
800
|
+
#pivotItem
|
801
|
+
|
802
|
+
get _pivotItem () {
|
803
|
+
if (typeof this.#pivotItem === 'undefined') {
|
804
|
+
this.#pivotItem = this._findPivotItem(this._item, 'previousElementSibling');
|
805
|
+
}
|
806
|
+
|
807
|
+
return this.#pivotItem
|
808
|
+
}
|
809
|
+
|
810
|
+
_move () {
|
811
|
+
this._pivotItem.before(this._item);
|
812
|
+
}
|
813
|
+
}
|
814
|
+
|
815
|
+
class Down extends Move {
|
816
|
+
/* Protected and private attributes and methods */
|
817
|
+
#pivotItem
|
818
|
+
|
819
|
+
get _pivotItem () {
|
820
|
+
if (typeof this.#pivotItem === 'undefined') {
|
821
|
+
this.#pivotItem = this._findPivotItem(this._item, 'nextElementSibling');
|
822
|
+
}
|
823
|
+
|
824
|
+
return this.#pivotItem
|
825
|
+
}
|
826
|
+
|
827
|
+
_move () {
|
828
|
+
this._pivotItem.after(this._item);
|
829
|
+
}
|
830
|
+
}
|
831
|
+
|
832
|
+
class Reindexer {
|
833
|
+
constructor (cocooned, startAt = 0) {
|
834
|
+
this.#cocooned = cocooned;
|
835
|
+
this.#startAt = startAt;
|
836
|
+
}
|
424
837
|
|
838
|
+
reindex (event) {
|
425
839
|
// Reindex can be prevented through a 'cocooned:before-reindex' event handler
|
426
|
-
if (!this
|
427
|
-
return false
|
840
|
+
if (!this.#notify('before-reindex', event)) {
|
841
|
+
return false
|
428
842
|
}
|
429
843
|
|
430
|
-
|
431
|
-
this
|
432
|
-
}
|
844
|
+
this.#positionFields.forEach((field, i) => field.setAttribute('value', i + this.#startAt));
|
845
|
+
this.#notify('after-reindex', event);
|
846
|
+
}
|
433
847
|
|
434
|
-
|
435
|
-
|
848
|
+
/* Protected and private attributes and methods */
|
849
|
+
#cocooned
|
850
|
+
#startAt
|
436
851
|
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
node.removeClass('cocooned-hidden-item');
|
441
|
-
}, 500);
|
442
|
-
},
|
852
|
+
get #positionFields () {
|
853
|
+
return this.#nodes.map(node => node.querySelector('input[name$="[position]"]'))
|
854
|
+
}
|
443
855
|
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
856
|
+
get #nodes () {
|
857
|
+
return this.#cocooned.items
|
858
|
+
}
|
859
|
+
|
860
|
+
#notify (eventName, originalEvent) {
|
861
|
+
return this.#cocooned.notify(this.#cocooned.container, eventName, this.#eventData(originalEvent))
|
862
|
+
}
|
863
|
+
|
864
|
+
#eventData (originalEvent) {
|
865
|
+
return { nodes: this.#nodes, cocooned: this.#cocooned, originalEvent }
|
866
|
+
}
|
867
|
+
}
|
868
|
+
|
869
|
+
function clickHandler (selector, cocooned, TriggerClass) {
|
870
|
+
return delegatedClickHandler(selector, (e) => {
|
871
|
+
const trigger = new TriggerClass(e.target, cocooned);
|
872
|
+
trigger.handle(e);
|
873
|
+
})
|
874
|
+
}
|
875
|
+
|
876
|
+
const reorderableMixin = (Base) => class extends Base {
|
877
|
+
static get defaultOptions () {
|
878
|
+
return { ...super.defaultOptions, ...{ reorderable: false } }
|
879
|
+
}
|
880
|
+
|
881
|
+
static get selectors () {
|
882
|
+
return {
|
883
|
+
...super.selectors,
|
884
|
+
'triggers.up': ['[data-cocooned-trigger="up"]', '.cocooned-move-up'],
|
885
|
+
'triggers.down': ['[data-cocooned-trigger="down"]', '.cocooned-move-down']
|
450
886
|
}
|
451
887
|
}
|
452
|
-
};
|
453
888
|
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
if (typeof container.data('cocooned') !== 'undefined') {
|
459
|
-
return;
|
889
|
+
start () {
|
890
|
+
super.start();
|
891
|
+
if (this.options.reorderable === false) {
|
892
|
+
return
|
460
893
|
}
|
461
894
|
|
462
|
-
|
463
|
-
|
464
|
-
|
895
|
+
this.container.addEventListener('cocooned:after-insert', e => this._reindexer.reindex(e));
|
896
|
+
this.container.addEventListener('cocooned:after-remove', e => this._reindexer.reindex(e));
|
897
|
+
this.container.addEventListener('cocooned:after-move', e => this._reindexer.reindex(e));
|
898
|
+
const form = this.container.closest('form');
|
899
|
+
if (form !== null) {
|
900
|
+
form.addEventListener('submit', e => this._reindexer.reindex(e));
|
465
901
|
}
|
466
902
|
|
467
|
-
|
468
|
-
container.
|
469
|
-
}
|
903
|
+
this.container.addEventListener('click', clickHandler(this._selector('triggers.up'), this, Up));
|
904
|
+
this.container.addEventListener('click', clickHandler(this._selector('triggers.down'), this, Down));
|
905
|
+
}
|
906
|
+
|
907
|
+
/* Protected and private attributes and methods */
|
908
|
+
static _normalizeOptions (options) {
|
909
|
+
const normalized = super._normalizeOptions(options);
|
910
|
+
if (typeof normalized.reorderable === 'boolean' && normalized.reorderable) {
|
911
|
+
normalized.reorderable = { startAt: 1 };
|
912
|
+
}
|
913
|
+
|
914
|
+
return normalized
|
915
|
+
}
|
916
|
+
|
917
|
+
#reindexer
|
918
|
+
|
919
|
+
get _reindexer () {
|
920
|
+
if (typeof this.#reindexer === 'undefined') {
|
921
|
+
this.#reindexer = new Reindexer(this, this.options.reorderable.startAt);
|
922
|
+
}
|
923
|
+
|
924
|
+
return this.#reindexer
|
925
|
+
}
|
926
|
+
};
|
927
|
+
|
928
|
+
const cocoonSupportMixin = (Base) => class extends Base {
|
929
|
+
static get eventNamespaces () {
|
930
|
+
return [...super.eventNamespaces, 'cocoon']
|
931
|
+
}
|
932
|
+
|
933
|
+
static get selectors () {
|
934
|
+
const selectors = super.selectors;
|
935
|
+
selectors.item.push('.nested-fields');
|
936
|
+
selectors['triggers.add'].push('.add_fields');
|
937
|
+
selectors['triggers.remove'].push('.remove_fields');
|
938
|
+
|
939
|
+
return selectors
|
940
|
+
}
|
941
|
+
};
|
942
|
+
|
943
|
+
const findInsertionNode = function (trigger, $) {
|
944
|
+
const insertionNode = trigger.data('association-insertion-node');
|
945
|
+
const insertionTraversal = trigger.data('association-insertion-traversal');
|
946
|
+
|
947
|
+
if (!insertionNode) return trigger.parent()
|
948
|
+
if (typeof insertionNode === 'function') return insertionNode(trigger)
|
949
|
+
if (insertionTraversal) return trigger[insertionTraversal](insertionNode)
|
950
|
+
return insertionNode === 'this' ? trigger : $(insertionNode)
|
951
|
+
};
|
952
|
+
|
953
|
+
const findContainer = function (trigger, $) {
|
954
|
+
const $trigger = $(trigger);
|
955
|
+
const insertionNode = findInsertionNode($trigger, $);
|
956
|
+
const insertionMethod = $trigger.data('association-insertion-method') || 'before';
|
957
|
+
|
958
|
+
if (['before', 'after', 'replaceWith'].includes(insertionMethod)) return insertionNode.parent()
|
959
|
+
return insertionNode
|
960
|
+
};
|
961
|
+
|
962
|
+
const cocoonAutoStart = function (jQuery) {
|
963
|
+
jQuery('.add_fields')
|
964
|
+
.map((_i, adder) => findContainer(adder, jQuery))
|
965
|
+
.each((_i, container) => jQuery(container).cocooned());
|
470
966
|
};
|
471
967
|
|
968
|
+
class Cocooned extends reorderableMixin(limitMixin(cocoonSupportMixin(Cocooned$1))) {
|
969
|
+
static create (container, options = {}) {
|
970
|
+
if ('cocoonedUuid' in container.dataset) {
|
971
|
+
return Cocooned.getInstance(container.dataset.cocoonedUuid)
|
972
|
+
}
|
973
|
+
|
974
|
+
const cocooned = new Cocooned(container, options);
|
975
|
+
cocooned.start();
|
976
|
+
|
977
|
+
return cocooned
|
978
|
+
}
|
979
|
+
|
980
|
+
static start () {
|
981
|
+
document.querySelectorAll('[data-cocooned-container], [data-cocooned-options]')
|
982
|
+
.forEach(element => Cocooned.create(element));
|
983
|
+
}
|
984
|
+
}
|
985
|
+
|
986
|
+
const jQueryPluginMixin = function (jQuery, Cocooned) {
|
987
|
+
jQuery.fn.cocooned = function (options) {
|
988
|
+
return this.each((_i, el) => Cocooned.create(el, options))
|
989
|
+
};
|
990
|
+
};
|
991
|
+
|
992
|
+
/* global jQuery, $ */
|
993
|
+
|
994
|
+
// Expose a jQuery plugin
|
995
|
+
jQueryPluginMixin(jQuery, Cocooned);
|
996
|
+
|
472
997
|
// On-load initialization
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
998
|
+
const cocoonedAutoStart = () => Cocooned.start();
|
999
|
+
$(cocoonedAutoStart);
|
1000
|
+
|
1001
|
+
$(() => cocoonAutoStart($));
|
1002
|
+
|
1003
|
+
deprecator('3.0').warn(
|
1004
|
+
'Loading @notus.sh/cocooned/cocooned is deprecated',
|
1005
|
+
'@notus.sh/cocooned/jquery, @notus.sh/cocooned or `@notus.sh/cocooned/src/cocooned/cocooned`'
|
1006
|
+
);
|
478
1007
|
|
479
1008
|
return Cocooned;
|
1009
|
+
|
480
1010
|
}));
|