active_element 0.0.19 → 0.0.21

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +4 -4
  3. data/app/assets/javascripts/active_element/active_element.js +1 -0
  4. data/app/assets/javascripts/active_element/ae-ujs.js +531 -0
  5. data/app/assets/javascripts/active_element/secret.js +3 -0
  6. data/app/controllers/active_element/application_controller.rb +2 -2
  7. data/app/views/active_element/components/button.html.erb +1 -1
  8. data/app/views/active_element/components/form.html.erb +16 -6
  9. data/app/views/active_element/components/table/_collection_row.html.erb +1 -1
  10. data/app/views/active_element/components/table/collection.html.erb +6 -1
  11. data/app/views/active_element/default_views/edit.html.erb +2 -2
  12. data/app/views/active_element/default_views/index.html.erb +13 -2
  13. data/app/views/active_element/default_views/show.html.erb +2 -2
  14. data/app/views/layouts/active_element.html.erb +9 -1
  15. data/config/routes.rb +1 -0
  16. data/example_app/Gemfile.lock +1 -1
  17. data/lib/active_element/components/button.rb +18 -3
  18. data/lib/active_element/components/collection_table.rb +4 -2
  19. data/lib/active_element/components/form.rb +19 -3
  20. data/lib/active_element/components/util/default_display_value.rb +4 -0
  21. data/lib/active_element/components/util/form_field_mapping.rb +5 -2
  22. data/lib/active_element/components/util/record_mapping.rb +10 -1
  23. data/lib/active_element/controller_interface.rb +2 -1
  24. data/lib/active_element/controller_state.rb +1 -1
  25. data/lib/active_element/default_controller/controller.rb +37 -3
  26. data/lib/active_element/default_controller/search.rb +15 -1
  27. data/lib/active_element/field_options.rb +3 -3
  28. data/lib/active_element/version.rb +1 -1
  29. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f38b4565ed14ef722b7acd88d3c75742d4f07bdd0ba1e83459d4ebeed6e2adf3
4
- data.tar.gz: cbc7e900bf6d78d571602c172e6dcb535a14d328b426477d32ad0197ff584f3b
3
+ metadata.gz: 61176f5d488163d3f19599507ea4c7a2af3652f682df78e825efb199b1b2099d
4
+ data.tar.gz: 41af730b7f551b759c79cb0a541e1ba6732d4e45182b902987fbe4c92e42df0c
5
5
  SHA512:
6
- metadata.gz: e9561a790971bfc4a71d4fc18269f3b6a5972e3bcc788f12d448987d0483014a733eb16e04bfc3206f6c45a296fc2a5720955ddc4e45ed93261a72954013a64f
7
- data.tar.gz: c0a6457abe28261de7f42e9b9e0802c04f6e40363cc92b72bce25736ddcd9dbbbf858a253ce5a0c083baa943ba72301110473cc2566303d5118fb5c66fb41df0
6
+ metadata.gz: db6f404ef4eda9dff01f1dfb3c6462cc5a46af1b27889be6dd46984404b478143b9c5d3ce939bbb7e96a251038984f4fc6f98eef73066188916803143b6f8357
7
+ data.tar.gz: 4be7450ece40217768ce7ce6a8fee08c830438039177ebbeb40f228b0d2fc406cd8dd6417dcab9aba2222a28e902b4548ad7cbf7c3036cd71fc25cd88f791c12
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- active_element (0.0.19)
4
+ active_element (0.0.20)
5
5
  bootstrap (~> 5.3.0alpha3)
6
6
  kaminari (~> 1.2)
7
7
  paintbrush (~> 0.1.2)
@@ -83,7 +83,7 @@ GEM
83
83
  autoprefixer-rails (10.4.16.0)
84
84
  execjs (~> 2)
85
85
  bcrypt (3.1.18)
86
- bootstrap (5.3.2)
86
+ bootstrap (5.3.3)
87
87
  autoprefixer-rails (>= 9.1.0)
88
88
  popper_js (>= 2.11.8, < 3)
89
89
  brakeman (5.4.1)
@@ -150,7 +150,7 @@ GEM
150
150
  mini_mime (1.1.5)
151
151
  mini_portile2 (2.8.2)
152
152
  minitest (5.18.1)
153
- net-imap (0.4.10)
153
+ net-imap (0.4.11)
154
154
  date
155
155
  net-protocol
156
156
  net-pop (0.1.2)
@@ -159,7 +159,7 @@ GEM
159
159
  timeout
160
160
  net-smtp (0.5.0)
161
161
  net-protocol
162
- nio4r (2.7.1)
162
+ nio4r (2.7.3)
163
163
  nokogiri (1.15.2)
164
164
  mini_portile2 (~> 2.8.2)
165
165
  racc (~> 1.4)
@@ -10,3 +10,4 @@
10
10
  //= require active_element/toast
11
11
  //= require active_element/popover
12
12
  //= require active_element/timezones
13
+ //= require active_element/ae-ujs
@@ -0,0 +1,531 @@
1
+ /*
2
+ Unobtrusive JavaScript
3
+ https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts
4
+ Released under the MIT license
5
+ */;
6
+
7
+ /* Reduced and customized for ActiveElementUJS */
8
+
9
+ (function() {
10
+ var context = this;
11
+
12
+ (function() {
13
+ (function() {
14
+ this.ActiveElementUJS = { linkClickSelector: 'a[data-ae-method]' };
15
+ }).call(this);
16
+ }).call(context);
17
+
18
+ var ActiveElementUJS = context.ActiveElementUJS;
19
+
20
+ (function() {
21
+ (function() {
22
+ var nonce;
23
+
24
+ nonce = null;
25
+
26
+ ActiveElementUJS.loadCSPNonce = function() {
27
+ var ref;
28
+ return nonce = (ref = document.querySelector("meta[name=csp-nonce]")) != null ? ref.content : void 0;
29
+ };
30
+
31
+ ActiveElementUJS.cspNonce = function() {
32
+ return nonce != null ? nonce : ActiveElementUJS.loadCSPNonce();
33
+ };
34
+
35
+ }).call(this);
36
+ (function() {
37
+ var expando, m;
38
+
39
+ m = Element.prototype.matches || Element.prototype.matchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.webkitMatchesSelector;
40
+
41
+ ActiveElementUJS.matches = function(element, selector) {
42
+ if (selector.exclude != null) {
43
+ return m.call(element, selector.selector) && !m.call(element, selector.exclude);
44
+ } else {
45
+ return m.call(element, selector);
46
+ }
47
+ };
48
+
49
+ expando = '_ujsData';
50
+
51
+ ActiveElementUJS.getData = function(element, key) {
52
+ var ref;
53
+ return (ref = element[expando]) != null ? ref[key] : void 0;
54
+ };
55
+
56
+ ActiveElementUJS.setData = function(element, key, value) {
57
+ if (element[expando] == null) {
58
+ element[expando] = {};
59
+ }
60
+ return element[expando][key] = value;
61
+ };
62
+
63
+ ActiveElementUJS.$ = function(selector) {
64
+ return Array.prototype.slice.call(document.querySelectorAll(selector));
65
+ };
66
+
67
+ }).call(this);
68
+ (function() {
69
+ var $, csrfParam, csrfToken;
70
+
71
+ $ = ActiveElementUJS.$;
72
+
73
+ csrfToken = ActiveElementUJS.csrfToken = function() {
74
+ var meta;
75
+ meta = document.querySelector('meta[name=csrf-token]');
76
+ return meta && meta.content;
77
+ };
78
+
79
+ csrfParam = ActiveElementUJS.csrfParam = function() {
80
+ var meta;
81
+ meta = document.querySelector('meta[name=csrf-param]');
82
+ return meta && meta.content;
83
+ };
84
+
85
+ ActiveElementUJS.CSRFProtection = function(xhr) {
86
+ var token;
87
+ token = csrfToken();
88
+ if (token != null) {
89
+ return xhr.setRequestHeader('X-CSRF-Token', token);
90
+ }
91
+ };
92
+
93
+ ActiveElementUJS.refreshCSRFTokens = function() {
94
+ var param, token;
95
+ token = csrfToken();
96
+ param = csrfParam();
97
+ if ((token != null) && (param != null)) {
98
+ return $('form input[name="' + param + '"]').forEach(function(input) {
99
+ return input.value = token;
100
+ });
101
+ }
102
+ };
103
+
104
+ }).call(this);
105
+ (function() {
106
+ var CustomEvent, fire, matches, preventDefault;
107
+
108
+ matches = ActiveElementUJS.matches;
109
+
110
+ CustomEvent = window.CustomEvent;
111
+
112
+ if (typeof CustomEvent !== 'function') {
113
+ CustomEvent = function(event, params) {
114
+ var evt;
115
+ evt = document.createEvent('CustomEvent');
116
+ evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
117
+ return evt;
118
+ };
119
+ CustomEvent.prototype = window.Event.prototype;
120
+ preventDefault = CustomEvent.prototype.preventDefault;
121
+ CustomEvent.prototype.preventDefault = function() {
122
+ var result;
123
+ result = preventDefault.call(this);
124
+ if (this.cancelable && !this.defaultPrevented) {
125
+ Object.defineProperty(this, 'defaultPrevented', {
126
+ get: function() {
127
+ return true;
128
+ }
129
+ });
130
+ }
131
+ return result;
132
+ };
133
+ }
134
+
135
+ fire = ActiveElementUJS.fire = function(obj, name, data) {
136
+ var event;
137
+ event = new CustomEvent(name, {
138
+ bubbles: true,
139
+ cancelable: true,
140
+ detail: data
141
+ });
142
+ obj.dispatchEvent(event);
143
+ return !event.defaultPrevented;
144
+ };
145
+
146
+ ActiveElementUJS.stopEverything = function(e) {
147
+ fire(e.target, 'ujs:everythingStopped');
148
+ e.preventDefault();
149
+ e.stopPropagation();
150
+ return e.stopImmediatePropagation();
151
+ };
152
+
153
+ ActiveElementUJS.delegate = function(element, selector, eventType, handler) {
154
+ return element.addEventListener(eventType, function(e) {
155
+ var target;
156
+ target = e.target;
157
+ while (!(!(target instanceof Element) || matches(target, selector))) {
158
+ target = target.parentNode;
159
+ }
160
+ if (target instanceof Element && handler.call(target, e) === false) {
161
+ e.preventDefault();
162
+ return e.stopPropagation();
163
+ }
164
+ });
165
+ };
166
+
167
+ }).call(this);
168
+ (function() {
169
+ var AcceptHeaders, CSRFProtection, createXHR, cspNonce, fire, prepareOptions, processResponse;
170
+
171
+ cspNonce = ActiveElementUJS.cspNonce, CSRFProtection = ActiveElementUJS.CSRFProtection, fire = ActiveElementUJS.fire;
172
+
173
+ AcceptHeaders = {
174
+ '*': '*/*',
175
+ text: 'text/plain',
176
+ html: 'text/html',
177
+ xml: 'application/xml, text/xml',
178
+ json: 'application/json, text/javascript',
179
+ script: 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript'
180
+ };
181
+
182
+ ActiveElementUJS.ajax = function(options) {
183
+ var xhr;
184
+ options = prepareOptions(options);
185
+ xhr = createXHR(options, function() {
186
+ var ref, response;
187
+ response = processResponse((ref = xhr.response) != null ? ref : xhr.responseText, xhr.getResponseHeader('Content-Type'));
188
+ if (Math.floor(xhr.status / 100) === 2) {
189
+ if (typeof options.success === "function") {
190
+ options.success(response, xhr.statusText, xhr);
191
+ }
192
+ } else {
193
+ if (typeof options.error === "function") {
194
+ options.error(response, xhr.statusText, xhr);
195
+ }
196
+ }
197
+ return typeof options.complete === "function" ? options.complete(xhr, xhr.statusText) : void 0;
198
+ });
199
+ if ((options.beforeSend != null) && !options.beforeSend(xhr, options)) {
200
+ return false;
201
+ }
202
+ if (xhr.readyState === XMLHttpRequest.OPENED) {
203
+ return xhr.send(options.data);
204
+ }
205
+ };
206
+
207
+ prepareOptions = function(options) {
208
+ options.url = options.url || location.href;
209
+ options.type = options.type.toUpperCase();
210
+ if (options.type === 'GET' && options.data) {
211
+ if (options.url.indexOf('?') < 0) {
212
+ options.url += '?' + options.data;
213
+ } else {
214
+ options.url += '&' + options.data;
215
+ }
216
+ }
217
+ if (AcceptHeaders[options.dataType] == null) {
218
+ options.dataType = '*';
219
+ }
220
+ options.accept = AcceptHeaders[options.dataType];
221
+ if (options.dataType !== '*') {
222
+ options.accept += ', */*; q=0.01';
223
+ }
224
+ return options;
225
+ };
226
+
227
+ createXHR = function(options, done) {
228
+ var xhr;
229
+ xhr = new XMLHttpRequest();
230
+ xhr.open(options.type, options.url, true);
231
+ xhr.setRequestHeader('Accept', options.accept);
232
+ if (typeof options.data === 'string') {
233
+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
234
+ }
235
+ if (!options.crossDomain) {
236
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
237
+ CSRFProtection(xhr);
238
+ }
239
+ xhr.withCredentials = !!options.withCredentials;
240
+ xhr.onreadystatechange = function() {
241
+ if (xhr.readyState === XMLHttpRequest.DONE) {
242
+ return done(xhr);
243
+ }
244
+ };
245
+ return xhr;
246
+ };
247
+
248
+ processResponse = function(response, type) {
249
+ var parser, script;
250
+ if (typeof response === 'string' && typeof type === 'string') {
251
+ if (type.match(/\bjson\b/)) {
252
+ try {
253
+ response = JSON.parse(response);
254
+ } catch (error) {}
255
+ } else if (type.match(/\b(?:java|ecma)script\b/)) {
256
+ script = document.createElement('script');
257
+ script.setAttribute('nonce', cspNonce());
258
+ script.text = response;
259
+ document.head.appendChild(script).parentNode.removeChild(script);
260
+ } else if (type.match(/\b(xml|html|svg)\b/)) {
261
+ parser = new DOMParser();
262
+ type = type.replace(/;.+/, '');
263
+ try {
264
+ response = parser.parseFromString(response, type);
265
+ } catch (error) {}
266
+ }
267
+ }
268
+ return response;
269
+ };
270
+
271
+ ActiveElementUJS.href = function(element) {
272
+ return element.href;
273
+ };
274
+
275
+ ActiveElementUJS.isCrossDomain = function(url) {
276
+ var e, originAnchor, urlAnchor;
277
+ originAnchor = document.createElement('a');
278
+ originAnchor.href = location.href;
279
+ urlAnchor = document.createElement('a');
280
+ try {
281
+ urlAnchor.href = url;
282
+ return !(((!urlAnchor.protocol || urlAnchor.protocol === ':') && !urlAnchor.host) || (originAnchor.protocol + '//' + originAnchor.host === urlAnchor.protocol + '//' + urlAnchor.host));
283
+ } catch (error) {
284
+ e = error;
285
+ return true;
286
+ }
287
+ };
288
+
289
+ }).call(this);
290
+ (function() {
291
+ var matches, toArray;
292
+
293
+ matches = ActiveElementUJS.matches;
294
+
295
+ toArray = function(e) {
296
+ return Array.prototype.slice.call(e);
297
+ };
298
+
299
+ ActiveElementUJS.serializeElement = function(element, additionalParam) {
300
+ var inputs, params;
301
+ inputs = [element];
302
+ if (matches(element, 'form')) {
303
+ inputs = toArray(element.elements);
304
+ }
305
+ params = [];
306
+ inputs.forEach(function(input) {
307
+ if (!input.name || input.disabled) {
308
+ return;
309
+ }
310
+ if (matches(input, 'select')) {
311
+ return toArray(input.options).forEach(function(option) {
312
+ if (option.selected) {
313
+ return params.push({
314
+ name: input.name,
315
+ value: option.value
316
+ });
317
+ }
318
+ });
319
+ } else if (input.checked || ['radio', 'checkbox', 'submit'].indexOf(input.type) === -1) {
320
+ return params.push({
321
+ name: input.name,
322
+ value: input.value
323
+ });
324
+ }
325
+ });
326
+ if (additionalParam) {
327
+ params.push(additionalParam);
328
+ }
329
+ return params.map(function(param) {
330
+ if (param.name != null) {
331
+ return (encodeURIComponent(param.name)) + "=" + (encodeURIComponent(param.value));
332
+ } else {
333
+ return param;
334
+ }
335
+ }).join('&');
336
+ };
337
+
338
+ ActiveElementUJS.formElements = function(form, selector) {
339
+ if (matches(form, 'form')) {
340
+ return toArray(form.elements).filter(function(el) {
341
+ return matches(el, selector);
342
+ });
343
+ } else {
344
+ return toArray(form.querySelectorAll(selector));
345
+ }
346
+ };
347
+
348
+ }).call(this);
349
+ (function() {
350
+ var allowAction, fire, stopEverything;
351
+
352
+ fire = ActiveElementUJS.fire, stopEverything = ActiveElementUJS.stopEverything;
353
+
354
+ ActiveElementUJS.handleConfirm = function(e) {
355
+ if (!allowAction(this)) {
356
+ return stopEverything(e);
357
+ }
358
+ };
359
+
360
+ allowAction = function(element) {
361
+ var answer, callback, message;
362
+ message = element.getAttribute('data-confirm');
363
+ if (!message) {
364
+ return true;
365
+ }
366
+ answer = false;
367
+ if (fire(element, 'confirm')) {
368
+ try {
369
+ answer = confirm(message);
370
+ } catch (error) {}
371
+ callback = fire(element, 'confirm:complete', [answer]);
372
+ }
373
+ return answer && callback;
374
+ };
375
+
376
+ }).call(this);
377
+
378
+ (function() {
379
+ var stopEverything;
380
+
381
+ stopEverything = ActiveElementUJS.stopEverything;
382
+
383
+ ActiveElementUJS.handleMethod = function(e) {
384
+ var csrfParam, csrfToken, form, formContent, href, link, method;
385
+ link = this;
386
+ method = link.getAttribute('data-ae-method');
387
+ if (!method) {
388
+ return;
389
+ }
390
+ href = ActiveElementUJS.href(link);
391
+ csrfToken = ActiveElementUJS.csrfToken();
392
+ csrfParam = ActiveElementUJS.csrfParam();
393
+ form = document.createElement('form');
394
+ formContent = "<input name='_method' value='" + method + "' type='hidden' />";
395
+ if ((csrfParam != null) && (csrfToken != null) && !ActiveElementUJS.isCrossDomain(href)) {
396
+ formContent += "<input name='" + csrfParam + "' value='" + csrfToken + "' type='hidden' />";
397
+ }
398
+ formContent += '<input type="submit" />';
399
+ form.method = 'post';
400
+ form.action = href;
401
+ form.target = link.target;
402
+ form.innerHTML = formContent;
403
+ form.style.display = 'none';
404
+ document.body.appendChild(form);
405
+ form.querySelector('[type="submit"]').click();
406
+ return stopEverything(e);
407
+ };
408
+
409
+ }).call(this);
410
+ (function() {
411
+ var ajax, fire, getData, isCrossDomain, isRemote, matches, serializeElement, setData, stopEverything,
412
+ slice = [].slice;
413
+
414
+ matches = ActiveElementUJS.matches, getData = ActiveElementUJS.getData, setData = ActiveElementUJS.setData, fire = ActiveElementUJS.fire, stopEverything = ActiveElementUJS.stopEverything, ajax = ActiveElementUJS.ajax, isCrossDomain = ActiveElementUJS.isCrossDomain, serializeElement = ActiveElementUJS.serializeElement;
415
+
416
+ isRemote = function(element) {
417
+ var value;
418
+ value = element.getAttribute('data-remote');
419
+ return (value != null) && value !== 'false';
420
+ };
421
+
422
+ ActiveElementUJS.handleRemote = function(e) {
423
+ var button, data, dataType, element, method, url, withCredentials;
424
+ element = this;
425
+ if (!isRemote(element)) {
426
+ return true;
427
+ }
428
+ if (!fire(element, 'ajax:before')) {
429
+ fire(element, 'ajax:stopped');
430
+ return false;
431
+ }
432
+ withCredentials = element.getAttribute('data-with-credentials');
433
+ dataType = element.getAttribute('data-type') || 'script';
434
+ method = element.getAttribute('data-ae-method');
435
+ url = ActiveElementUJS.href(element);
436
+ data = element.getAttribute('data-params');
437
+ ajax({
438
+ type: method || 'GET',
439
+ url: url,
440
+ data: data,
441
+ dataType: dataType,
442
+ beforeSend: function(xhr, options) {
443
+ if (fire(element, 'ajax:beforeSend', [xhr, options])) {
444
+ return fire(element, 'ajax:send', [xhr]);
445
+ } else {
446
+ fire(element, 'ajax:stopped');
447
+ return false;
448
+ }
449
+ },
450
+ success: function() {
451
+ var args;
452
+ args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
453
+ return fire(element, 'ajax:success', args);
454
+ },
455
+ error: function() {
456
+ var args;
457
+ args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
458
+ return fire(element, 'ajax:error', args);
459
+ },
460
+ complete: function() {
461
+ var args;
462
+ args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
463
+ return fire(element, 'ajax:complete', args);
464
+ },
465
+ crossDomain: isCrossDomain(url),
466
+ withCredentials: (withCredentials != null) && withCredentials !== 'false'
467
+ });
468
+ return stopEverything(e);
469
+ };
470
+
471
+ ActiveElementUJS.formSubmitButtonClick = function(e) {
472
+ var button, form;
473
+ button = this;
474
+ form = button.form;
475
+ if (!form) {
476
+ return;
477
+ }
478
+ if (button.name) {
479
+ setData(form, 'ujs:submit-button', {
480
+ name: button.name,
481
+ value: button.value
482
+ });
483
+ }
484
+ setData(form, 'ujs:formnovalidate-button', button.formNoValidate);
485
+ setData(form, 'ujs:submit-button-formaction', button.getAttribute('formaction'));
486
+ return setData(form, 'ujs:submit-button-formmethod', button.getAttribute('formmethod'));
487
+ };
488
+
489
+ ActiveElementUJS.preventInsignificantClick = function(e) {
490
+ var data, insignificantMetaClick, link, metaClick, method, nonPrimaryMouseClick;
491
+ link = this;
492
+ method = (link.getAttribute('data-ae-method') || 'GET').toUpperCase();
493
+ data = link.getAttribute('data-params');
494
+ metaClick = e.metaKey || e.ctrlKey;
495
+ insignificantMetaClick = metaClick && method === 'GET' && !data;
496
+ nonPrimaryMouseClick = (e.button != null) && e.button !== 0;
497
+ if (nonPrimaryMouseClick || insignificantMetaClick) {
498
+ return e.stopImmediatePropagation();
499
+ }
500
+ };
501
+
502
+ }).call(this);
503
+ (function() {
504
+ var $, CSRFProtection, delegate, disableElement, enableElement, fire, formSubmitButtonClick, getData, handleConfirm, handleDisabledElement, handleMethod, handleRemote, loadCSPNonce, preventInsignificantClick, refreshCSRFTokens;
505
+
506
+ fire = ActiveElementUJS.fire, delegate = ActiveElementUJS.delegate, getData = ActiveElementUJS.getData, $ = ActiveElementUJS.$, refreshCSRFTokens = ActiveElementUJS.refreshCSRFTokens, CSRFProtection = ActiveElementUJS.CSRFProtection, loadCSPNonce = ActiveElementUJS.loadCSPNonce, enableElement = ActiveElementUJS.enableElement, disableElement = ActiveElementUJS.disableElement, handleDisabledElement = ActiveElementUJS.handleDisabledElement, handleConfirm = ActiveElementUJS.handleConfirm, preventInsignificantClick = ActiveElementUJS.preventInsignificantClick, handleRemote = ActiveElementUJS.handleRemote, formSubmitButtonClick = ActiveElementUJS.formSubmitButtonClick, handleMethod = ActiveElementUJS.handleMethod;
507
+
508
+ ActiveElementUJS.start = function() {
509
+ if (window._active_element_loaded) {
510
+ throw new Error('active_element-ujs has already been loaded!');
511
+ }
512
+ delegate(document, ActiveElementUJS.linkClickSelector, 'click', handleMethod);
513
+ document.addEventListener('DOMContentLoaded', refreshCSRFTokens);
514
+ document.addEventListener('DOMContentLoaded', loadCSPNonce);
515
+ console.log("ActiveElement-ujs loaded.");
516
+ return window._active_element_loaded = true;
517
+ };
518
+
519
+ if (window.ActiveElementUJS === ActiveElementUJS && fire(document, 'rails:attachBindings')) {
520
+ ActiveElementUJS.start();
521
+ }
522
+
523
+ }).call(this);
524
+ }).call(this);
525
+
526
+ if (typeof module === "object" && module.exports) {
527
+ module.exports = ActiveElementUJS;
528
+ } else if (typeof define === "function" && define.amd) {
529
+ define(ActiveElementUJS);
530
+ }
531
+ }).call(this);
@@ -1,6 +1,8 @@
1
1
  (() => {
2
2
  const cloneElement = (id) => ActiveElement.cloneElement('secret', id);
3
3
 
4
+ if (window._active_element_secrets_loaded) return;
5
+
4
6
  window.addEventListener(ActiveElement.reloadEvent, () => {
5
7
  document.querySelectorAll('span[data-field-type="secret"]').forEach((element) => {
6
8
  const secret = element.dataset.secret;
@@ -36,5 +38,6 @@
36
38
  element.append(showButton);
37
39
  element.append(hideButton);
38
40
  });
41
+ window._active_element_secrets_loaded = true;
39
42
  });
40
43
  })();
@@ -27,8 +27,8 @@ module ActiveElement
27
27
  helper_method :active_element
28
28
  helper_method :render_active_element_hook
29
29
 
30
- def render_active_element_hook(hook)
31
- render_to_string partial: hook
30
+ def render_active_element_hook(hook, locals: {})
31
+ render_to_string partial: hook, locals: locals
32
32
  rescue ActionView::MissingTemplate
33
33
  nil
34
34
  end
@@ -6,7 +6,7 @@
6
6
  'bs-trigger': 'hover',
7
7
  'bs-toggle': 'popover',
8
8
  'bs-content': title,
9
- } : {}).merge(method&.to_s == 'get' ? {} : { 'turbo-method' => method.to_s }),
9
+ } : {}).merge(method&.to_s == 'get' ? {} : { 'ae-method' => method.to_s }),
10
10
  class: "btn #{button_class} #{float_class} #{kwargs_class}",
11
11
  **kwargs
12
12
  ) do %>
@@ -59,13 +59,23 @@
59
59
  <% fields.each_slice(columns) do |field_group| %>
60
60
  <div class="row form-fields mb-3">
61
61
  <% field_group.each do |field, type, options| %>
62
- <div class="col-sm-3">
63
- <%= render partial: 'active_element/components/form/label',
64
- locals: { component: component, id: id, type: type, form: form, field: field, options: options } %>
65
- </div>
62
+ <% if type != :hidden_field %>
63
+ <div class="col-sm-3">
64
+ <%= render partial: 'active_element/components/form/label',
65
+ locals: {
66
+ component: component,
67
+ id: id,
68
+ type: type,
69
+ form: form,
70
+ field: field,
71
+ options: options
72
+ } %>
73
+ </div>
74
+ <% end %>
66
75
 
67
76
 
68
- <div class="col">
77
+
78
+ <% if type != :hidden_field %><div class="col"><% end %>
69
79
  <%= render partial: 'active_element/components/form/field',
70
80
  locals: {
71
81
  id: id,
@@ -76,7 +86,7 @@
76
86
  component: component,
77
87
  record: record }
78
88
  %>
79
- </div>
89
+ <% if type != :hidden_field %></div><% end %>
80
90
  <% end %>
81
91
  </div>
82
92
  <% end %>
@@ -1,6 +1,6 @@
1
1
  <tr class="<%= (index % 2).zero? ? 'even' : 'odd' %> <%= row_class_mapper.call(item) %>">
2
2
  <% fields.each do |field, class_mapper, label, value_mapper| %>
3
- <td class="align-middle <%= class_mapper.call(item) %>">
3
+ <td class="align-top <%= class_mapper.call(item) %>">
4
4
  <% if component.secret_field?(field) %>
5
5
  <%= controller.helpers.render partial: 'active_element/components/secret/field',
6
6
  locals: { secret: value_mapper.call(item), label: label } %>
@@ -1,5 +1,10 @@
1
1
  <% if new %>
2
- <%= active_element.component.new_button(component.model&.new, float: 'end', class: 'mb-3') %>
2
+ <%= active_element.component.new_button(
3
+ component.model&.new,
4
+ nested_for: nested_for,
5
+ float: 'end',
6
+ class: 'mb-3'
7
+ ) %>
3
8
  <% end %>
4
9
 
5
10
 
@@ -1,9 +1,9 @@
1
1
  <%= active_element.component.page_title record.model_name.to_s.titleize %>
2
2
 
3
- <%= render_active_element_hook "#{controller_path}/before_edit" %>
3
+ <%= render_active_element_hook "#{controller_path}/before_edit", locals: { record: record } %>
4
4
 
5
5
  <%= active_element.component.form model: [namespace, record].compact,
6
6
  destroy: active_element.state.deletable?,
7
7
  fields: active_element.state.editable_fields %>
8
8
 
9
- <%= render_active_element_hook "#{controller_path}/after_edit" %>
9
+ <%= render_active_element_hook "#{controller_path}/after_edit", locals: { record: record } %>
@@ -7,7 +7,17 @@
7
7
  fields: active_element.state.searchable_fields %>
8
8
  <% end %>
9
9
 
10
- <%= render_active_element_hook "#{controller_path}/before_index" %>
10
+ <%= render_active_element_hook "#{controller_path}/before_index", locals: { collection: collection } %>
11
+
12
+ <% if nested_for.present? %>
13
+ <%=
14
+ active_element.component.page_section_title(
15
+ nested_for.map do |nested_for_record|
16
+ ActiveElement::Components::Util::DefaultDisplayValue.new(object: nested_for_record).value
17
+ end.join(', ')
18
+ )
19
+ %>
20
+ <% end %>
11
21
 
12
22
  <% if active_element.state.search_required && search_filters.compact_blank.blank? %>
13
23
  <% if active_element.state.creatable? %>
@@ -20,8 +30,9 @@
20
30
  show: active_element.state.viewable?,
21
31
  edit: active_element.state.editable?,
22
32
  destroy: active_element.state.deletable?,
33
+ nested_for: nested_for,
23
34
  collection: collection,
24
35
  fields: active_element.state.listable_fields %>
25
36
  <% end %>
26
37
 
27
- <%= render_active_element_hook "#{controller_path}/after_index" %>
38
+ <%= render_active_element_hook "#{controller_path}/after_index", locals: { collection: collection } %>
@@ -1,10 +1,10 @@
1
1
  <%= active_element.component.page_title record.model_name.to_s.titleize %>
2
2
 
3
- <%= render_active_element_hook "#{controller_path}/before_show" %>
3
+ <%= render_active_element_hook "#{controller_path}/before_show", locals: { record: record } %>
4
4
 
5
5
  <%= active_element.component.table item: record,
6
6
  edit: active_element.state.editable?,
7
7
  destroy: active_element.state.deletable?,
8
8
  fields: active_element.state.viewable_fields %>
9
9
 
10
- <%= render_active_element_hook "#{controller_path}/after_show" %>
10
+ <%= render_active_element_hook "#{controller_path}/after_show", locals: { record: record } %>
@@ -19,6 +19,15 @@
19
19
  </script>
20
20
  <% end %>
21
21
 
22
+ <% if respond_to?(:javascript_pack_tag) && defined? Webpacker %>
23
+ <%= begin
24
+ javascript_pack_tag 'application'
25
+ rescue Webpacker::Manifest::MissingEntryError
26
+ nil
27
+ end
28
+ %>
29
+ <% end %>
30
+
22
31
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
23
32
  <link rel="stylesheet"
24
33
  href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css">
@@ -116,6 +125,5 @@
116
125
  <%= javascript_include_tag 'active_element/active_element', 'data-turbo-track': 'reload', 'data-turbolinks-track': 'reload' %>
117
126
  <%= javascript_include_tag 'application', 'data-turbo-track': 'reload', 'data-turbolinks-track': 'reload' %>
118
127
  <% end %>
119
-
120
128
  </body>
121
129
  </html>
data/config/routes.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  ActiveElement::Engine.routes.draw do
4
4
  ActiveElement.eager_load_controllers
5
+ ActiveElement.eager_load_models
5
6
 
6
7
  ActiveElement::ApplicationController.descendants.map do |descendant|
7
8
  post "#{descendant.controller_path}/_active_element_text_search",
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- active_element (0.0.18)
4
+ active_element (0.0.20)
5
5
  bootstrap (~> 5.3.0alpha3)
6
6
  kaminari (~> 1.2)
7
7
  paintbrush (~> 0.1.2)
@@ -6,7 +6,7 @@ module ActiveElement
6
6
  class Button
7
7
  # rubocop:disable Metrics/MethodLength
8
8
  def initialize(controller, record, flag_or_options, confirm: false, type: :primary, method: nil,
9
- float: nil, icon: nil, tooltip: false, **kwargs, &block)
9
+ float: nil, icon: nil, tooltip: false, nested_for: nil, **kwargs, &block)
10
10
  @controller = controller
11
11
  @record = record.is_a?(ActiveRecord::Relation) ? record.klass.new : record
12
12
  @flag_or_options = flag_or_options
@@ -20,6 +20,7 @@ module ActiveElement
20
20
  @block_given = block_given?
21
21
  @content = block.call if block_given?
22
22
  @tooltip = tooltip
23
+ @nested_for = nested_for
23
24
  end
24
25
  # rubocop:enable Metrics/MethodLength
25
26
 
@@ -49,7 +50,7 @@ module ActiveElement
49
50
  private
50
51
 
51
52
  attr_reader :controller, :record, :flag_or_options, :float, :kwargs, :kwargs_class, :type, :method, :icon,
52
- :block_given, :content, :confirm, :tooltip
53
+ :block_given, :content, :confirm, :tooltip, :nested_for
53
54
 
54
55
  def link_method
55
56
  return method if method.present?
@@ -116,7 +117,21 @@ module ActiveElement
116
117
  def record_path
117
118
  return nil unless record.class.is_a?(ActiveModel::Naming)
118
119
 
119
- Util::RecordPath.new(record: record, controller: controller, type: type).path
120
+ Util::RecordPath.new(record: record, controller: controller, type: type).path(**nested_args)
121
+ end
122
+
123
+ def nested_args
124
+ case type
125
+ when :new
126
+ nested_params
127
+ else
128
+ {}
129
+ end
130
+ end
131
+
132
+ def nested_params
133
+ route = controller.request.routes.recognize_path(controller.request.path)
134
+ route.reject { |key, _value| %w[controller action].include?(key.to_s) }
120
135
  end
121
136
  end
122
137
  end
@@ -15,7 +15,7 @@ module ActiveElement
15
15
  # rubocop:disable Metrics/MethodLength
16
16
  def initialize(controller, class_name:, collection:, fields:, params:, model_name: nil, style: nil,
17
17
  show: false, new: false, edit: false, destroy: false, paginate: true, group: nil,
18
- group_title: false, row_class: nil, title: nil, **_kwargs)
18
+ group_title: false, nested_for: nil, row_class: nil, title: nil, **_kwargs)
19
19
  @controller = controller
20
20
  @class_name = class_name
21
21
  @model_name = model_name
@@ -32,6 +32,7 @@ module ActiveElement
32
32
  @group_title = group_title
33
33
  @row_class = row_class
34
34
  @title = title
35
+ @nested_for = nested_for
35
36
  verify_paginate_and_group
36
37
  end
37
38
  # rubocop:enable Metrics/MethodLength
@@ -54,6 +55,7 @@ module ActiveElement
54
55
  destroy: destroy,
55
56
  group: group,
56
57
  group_title: group_title,
58
+ nested_for: nested_for,
57
59
  display_pagination: display_pagination?,
58
60
  page_sizes: [5, 10, 25, 50, 75, 100, 200],
59
61
  page_size: page_size,
@@ -78,7 +80,7 @@ module ActiveElement
78
80
 
79
81
  attr_reader :class_name, :collection, :fields, :style, :row_class,
80
82
  :new, :show, :edit, :destroy,
81
- :paginate, :params, :group, :group_title, :title
83
+ :paginate, :params, :group, :group_title, :title, :nested_for
82
84
 
83
85
  def paginated_collection
84
86
  return collection unless paginate && collection.respond_to?(:page) && !limit?
@@ -190,14 +190,30 @@ module ActiveElement
190
190
  end
191
191
 
192
192
  def base_options_for_select(field, field_options)
193
- return normalized_options(field_options.fetch(:options)) if field_options.key?(:options)
193
+ return normalized_options(field_options.fetch(:options), field_options) if field_options.key?(:options)
194
194
  return default_options_for_select(field, field_options) if record.class.is_a?(ActiveModel::Naming)
195
195
 
196
196
  raise ArgumentError, "Must provide select options `[:#{field}, { options: [...] }]` or a record instance."
197
197
  end
198
198
 
199
- def normalized_options(options)
200
- options.map { |option| option.is_a?(Array) ? option : [option, option] }
199
+ def normalized_options(options, field_options)
200
+ options.map do |option|
201
+ next option if option.is_a?(Array)
202
+ next active_record_option(option, field_options) if option.is_a?(ActiveRecord::Base)
203
+ [option, option] if option.is_a?(String)
204
+ end
205
+ end
206
+
207
+ def active_record_option(option, field_options)
208
+ [active_record_display_value(option, field_options), option.send(option.class.primary_key)]
209
+ end
210
+
211
+ def active_record_display_value(option, field_options)
212
+ if field_options[:display_value].is_a?(Proc) && record.present?
213
+ field_options[:display_value].call(option)
214
+ else
215
+ Util::DefaultDisplayValue.new(object: option).value
216
+ end
201
217
  end
202
218
 
203
219
  def default_class_name
@@ -12,6 +12,10 @@ module ActiveElement
12
12
  end
13
13
 
14
14
  def value
15
+ if object.respond_to?(:default_display_attribute)
16
+ return object.public_send(object.default_display_attribute)
17
+ end
18
+
15
19
  DEFAULT_FIELDS.each do |field|
16
20
  return object.public_send(field) if active_record_value?(field)
17
21
  return object[field] if hash_key(field) if hash_value?(field)
@@ -59,10 +59,12 @@ module ActiveElement
59
59
  end
60
60
 
61
61
  def inline_configured_field(field)
62
- field_options = FieldOptions.from_state(field, controller.active_element.state, record)
62
+ field_options = FieldOptions.from_state(
63
+ field, controller.active_element.state, record, controller
64
+ )
63
65
  return nil if field_options.blank?
64
66
 
65
- [field, field_options.type, field_options.options]
67
+ [field, field_options.type, field_options.options.reverse_merge({ value: field_options.value })]
66
68
  end
67
69
 
68
70
  def field_with_provided_type_and_provided_options(field)
@@ -201,6 +203,7 @@ module ActiveElement
201
203
  json: :json_field,
202
204
  jsonb: :json_field,
203
205
  geometry: :text_area,
206
+ text: :text_area,
204
207
  datetime: :datetime_field,
205
208
  date: :date_field,
206
209
  time: :time_field,
@@ -80,13 +80,22 @@ module ActiveElement
80
80
  end
81
81
 
82
82
  def value_from_config
83
- field_options = FieldOptions.from_state(field, component.controller.active_element.state, record)
83
+ field_options = field_options_from_state
84
84
  return nil if field_options.blank?
85
85
  return nil unless DATABASE_TYPES.include?(field_options.type.to_sym)
86
86
 
87
87
  send("#{field_options.type}_value")
88
88
  end
89
89
 
90
+ def field_options_from_state
91
+ FieldOptions.from_state(
92
+ field,
93
+ component.controller.active_element.state,
94
+ record,
95
+ component.controller
96
+ )
97
+ end
98
+
90
99
  # Override these methods as required in a class that includes this module:
91
100
 
92
101
  def mapped_association_from_record
@@ -25,8 +25,9 @@ module ActiveElement
25
25
  @authorize
26
26
  end
27
27
 
28
- def listable_fields(*args, order: nil)
28
+ def listable_fields(*args, order: nil, scope: nil)
29
29
  state.list_order = order
30
+ state.list_scope = scope
30
31
  state.listable_fields.concat(args.map(&:to_sym)).uniq!
31
32
  end
32
33
 
@@ -8,7 +8,7 @@ module ActiveElement
8
8
  attr_reader :permissions, :listable_fields, :viewable_fields, :editable_fields, :searchable_fields,
9
9
  :field_options
10
10
  attr_accessor :sign_in_path, :sign_in, :sign_in_method, :sign_out_path, :sign_out_method,
11
- :deletable, :authorizor, :authenticator, :list_order, :search_required, :model
11
+ :deletable, :authorizor, :authenticator, :list_order, :list_scope, :search_required, :model
12
12
 
13
13
  def initialize(controller:)
14
14
  @controller = controller
@@ -15,7 +15,8 @@ module ActiveElement
15
15
  controller.render 'active_element/default_views/index',
16
16
  locals: {
17
17
  collection: ordered(collection),
18
- search_filters: default_text_search.search_filters
18
+ search_filters: default_text_search.search_filters,
19
+ nested_for: nested_relations
19
20
  }
20
21
  end
21
22
 
@@ -124,9 +125,12 @@ module ActiveElement
124
125
  end
125
126
 
126
127
  def collection
127
- return model.all unless default_text_search.text_search?
128
+ return model.public_send(list_scope).where(nested_scope) unless default_text_search.text_search?
128
129
 
129
- model.left_outer_joins(default_text_search.search_relations).where(*default_text_search.text_search)
130
+ model.public_send(list_scope)
131
+ .left_outer_joins(default_text_search.search_relations)
132
+ .where(nested_scope)
133
+ .where(*default_text_search.text_search)
130
134
  end
131
135
 
132
136
  def render_range_error(error:, action:)
@@ -140,6 +144,36 @@ module ActiveElement
140
144
 
141
145
  I18n.t('active_element.unexpected_error')
142
146
  end
147
+
148
+ def list_scope
149
+ return :all if state.list_scope.blank?
150
+ return state.list_scope.call(request) if state.list_scope.is_a?(Proc)
151
+
152
+ state.list_scope
153
+ end
154
+
155
+ def nested_scope
156
+ nested_params.presence || noop
157
+ end
158
+
159
+ def noop
160
+ Arel::Nodes::True.new.eq(Arel::Nodes::True.new)
161
+ end
162
+
163
+ def nested_params
164
+ route = controller.request.routes.recognize_path(controller.request.path)
165
+ route.reject { |key, _value| %w[controller action].include?(key.to_s) }
166
+ end
167
+
168
+ def nested_relations
169
+ return [] if nested_params.blank?
170
+
171
+ nested_params.map do |key, value|
172
+ collection.model.reflections.values.find do |reflection|
173
+ reflection.foreign_key.to_s == key.to_s
174
+ end&.klass&.find(value)
175
+ end.compact
176
+ end
143
177
  end
144
178
  end
145
179
  end
@@ -25,6 +25,7 @@ module ActiveElement
25
25
  conditions = search_filters.to_h.map do |key, value|
26
26
  next relation_matches(key, value) if relation?(key)
27
27
  next datetime_between(key, value) if datetime?(key)
28
+ next join(key, value) if key.to_s.include?('.')
28
29
  next model.arel_table[key].matches("#{value}%") if string_like_column?(key)
29
30
 
30
31
  model.arel_table[key].eq(value)
@@ -36,7 +37,15 @@ module ActiveElement
36
37
  end
37
38
 
38
39
  def search_relations
39
- search_filters.to_h.keys.map { |key| relation?(key) ? key.to_sym : nil }.compact
40
+ relation_joins = search_filters.to_h.keys.map { |key| relation?(key) ? key.to_sym : nil }.compact
41
+ (relation_joins + shorthand_joins).uniq
42
+ end
43
+
44
+ def shorthand_joins
45
+ search_filters.to_h
46
+ .keys
47
+ .select { |key| key.to_s.include?('.') }
48
+ .map { |key| key.partition('.').first.to_sym }
40
49
  end
41
50
 
42
51
  private
@@ -66,6 +75,11 @@ module ActiveElement
66
75
  end.compact
67
76
  end
68
77
 
78
+ def join(key, value)
79
+ table, _, column = key.to_s.partition('.')
80
+ relation(table).klass.arel_table[column].eq(value)
81
+ end
82
+
69
83
  def noop
70
84
  Arel::Nodes::True.new.eq(Arel::Nodes::True.new)
71
85
  end
@@ -1,14 +1,14 @@
1
1
  module ActiveElement
2
2
  class FieldOptions
3
- attr_accessor :type, :options
3
+ attr_accessor :type, :options, :value
4
4
  attr_reader :field
5
5
 
6
- def self.from_state(field, state, record)
6
+ def self.from_state(field, state, record, controller)
7
7
  block = state.field_options[field]
8
8
  return nil if block.blank?
9
9
 
10
10
  field_options = new(field)
11
- block.call(field_options, record)
11
+ block.call(field_options, record, controller)
12
12
  field_options
13
13
  end
14
14
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveElement
4
- VERSION = '0.0.19'
4
+ VERSION = '0.0.21'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_element
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.19
4
+ version: 0.0.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Farrell
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-04-08 00:00:00.000000000 Z
11
+ date: 2024-05-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bootstrap
@@ -114,6 +114,7 @@ files:
114
114
  - active_element.gemspec
115
115
  - app/assets/config/active_element/manifest.js
116
116
  - app/assets/javascripts/active_element/active_element.js
117
+ - app/assets/javascripts/active_element/ae-ujs.js
117
118
  - app/assets/javascripts/active_element/confirm.js
118
119
  - app/assets/javascripts/active_element/form.js
119
120
  - app/assets/javascripts/active_element/highlight.js