formstrap 0.2.1 → 0.3.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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/app/assets/javascripts/formstrap/controllers/nested_preview_controller.js +167 -0
  4. data/app/assets/javascripts/formstrap/controllers/preview_controller.js +65 -0
  5. data/app/assets/javascripts/formstrap/controllers/repeater_controller.js +39 -7
  6. data/app/assets/javascripts/formstrap/index.js +4 -0
  7. data/app/assets/javascripts/formstrap.js +199 -7
  8. data/app/assets/stylesheets/formstrap/shared/nested_preview.scss +31 -0
  9. data/app/assets/stylesheets/formstrap/shared.scss +1 -0
  10. data/app/assets/stylesheets/formstrap.css +31 -0
  11. data/app/views/formstrap/_button.html.erb +0 -0
  12. data/app/views/formstrap/_repeater.html.erb +40 -22
  13. data/app/views/formstrap/repeater/_row.html.erb +20 -4
  14. data/app/views/formstrap/shared/_nested_preview.html.erb +35 -0
  15. data/config/locales/de.yml +5 -0
  16. data/config/locales/en.yml +1 -5
  17. data/config/locales/formstrap/de.yml +48 -0
  18. data/config/locales/formstrap/en.yml +48 -0
  19. data/config/locales/formstrap/fr.yml +48 -0
  20. data/config/locales/formstrap/nl.yml +48 -0
  21. data/config/locales/fr.yml +5 -0
  22. data/config/locales/nl.yml +1 -5
  23. data/lib/formstrap/form_builder.rb +15 -0
  24. data/lib/formstrap/version.rb +1 -1
  25. metadata +13 -8
  26. data/config/locales/formstrap/forms/en.yml +0 -25
  27. data/config/locales/formstrap/forms/nl.yml +0 -25
  28. data/config/locales/formstrap/media/en.yml +0 -16
  29. data/config/locales/formstrap/media/nl.yml +0 -16
  30. data/config/locales/formstrap/thumbnail/en.yml +0 -4
  31. data/config/locales/formstrap/thumbnail/nl.yml +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a60b106c9ba61e0e8e4bcfbff6922ee35ca94e0f9d6b323f2536bbdeb4b159dc
4
- data.tar.gz: b0df7426b57f7212e0fb4ba958feafab579fecffbd170a0b7519ea899a71846f
3
+ metadata.gz: 8243dd5f8e2fdae1537ffab42b56734ebdfe53e054feb92c68a323954c0a63e0
4
+ data.tar.gz: 0c5a4a73c1d4c518c2ee71cf36f3ccc882ee106a825d94aef212a639ee0bcb94
5
5
  SHA512:
6
- metadata.gz: 4bee48dada36d1789b7d913af8e22a4805bf2ffff7883f3e9a280d28799556abd9525045ff7395165255dfa84f5782bbde9c29d9855f61e8677b4d8cfa89120b
7
- data.tar.gz: 8c36be158e08f4fdfa85e990f5e74c0cf5e355a172303e7eb2c65214b13267ab5882ac0d873c77b2cb119f01e761c47a0761bd9b97f7d0a47a9d192d733e478f
6
+ metadata.gz: 6dbd8118d294ff8a863675299ce236cc3bd7f7a749b958b42cd22ac66465c3a5c751433edb93009a87b203d28995e2581b3f3600160d4f6a68e0b78110ab9fbe
7
+ data.tar.gz: 90ea13586df35f2db36e0b5f3139c6316ae5be21cf022355c665084a19e5d69663efe329f948bd6287b8deb8c4af76ac4b186a94b73aae5b73c8f6aded3659f9
data/README.md CHANGED
@@ -72,6 +72,7 @@ An overview of all the Formstrap / Ruby on Rails form helpers:
72
72
  | Textarea | textarea | textarea formstrap: false or text_area |
73
73
  | URL | url | url formstrap: false or url_field |
74
74
  | WYSIWYG * | wysiwyg | N/A |
75
+ | Repeater | repeater_for | Adds advanced features to fields_for |
75
76
 
76
77
  \* Formstrap provides the implementation of these 3rd party libraries, however it is up to the user to provide the
77
78
  correct assets.
@@ -0,0 +1,167 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller {
4
+ static get targets () {
5
+ return ['fields', 'iframeWrapper', 'iframe', 'offcanvas', 'error', 'loader']
6
+ }
7
+
8
+ static get values () {
9
+ return {
10
+ url: String
11
+ }
12
+ }
13
+
14
+ connect () {
15
+ this.prepareIframe()
16
+
17
+ // Resize iFrame after content is loaded
18
+ this.iframeTarget.addEventListener('load', () => {
19
+ this.hideLoader()
20
+ this.resizeIframe()
21
+ })
22
+
23
+ // Offcanvas closes
24
+ this.offcanvasTarget.addEventListener('hide.bs.offcanvas', (event) => {
25
+ if (!this.update()) {
26
+ event.preventDefault()
27
+ }
28
+ })
29
+ }
30
+
31
+ showLoader () {
32
+ this.loaderTarget.classList.remove('d-none')
33
+ }
34
+
35
+ hideLoader () {
36
+ this.loaderTarget.classList.add('d-none')
37
+ }
38
+
39
+ showError () {
40
+ this.errorTarget.classList.remove('d-none')
41
+ }
42
+
43
+ hideError () {
44
+ this.errorTarget.classList.add('d-none')
45
+ }
46
+
47
+ update () {
48
+ // Validate fields
49
+ const isValid = this.validateFields()
50
+
51
+ if (isValid) {
52
+ this.requestPreview()
53
+ this.hideError()
54
+ return true
55
+ } else {
56
+ this.showError()
57
+ return false
58
+ }
59
+ }
60
+
61
+ requestPreview () {
62
+ // Create an AJAX request
63
+ // eslint-disable-next-line no-undef
64
+ const xhr = new XMLHttpRequest()
65
+ xhr.open('POST', this.urlValue, true)
66
+
67
+ // Submit the form data
68
+ const formData = this.buildFormData()
69
+ xhr.send(formData)
70
+
71
+ // Show loader
72
+ this.showLoader()
73
+
74
+ // Handle the request once it's done
75
+ xhr.onreadystatechange = () => {
76
+ if (xhr.readyState === XMLHttpRequest.DONE) {
77
+ this.hideLoader()
78
+ this.handleRequest(xhr)
79
+ }
80
+ }
81
+ }
82
+
83
+ handleRequest (request) {
84
+ // Handle the response
85
+ if (request.status === 200) {
86
+ this.updatePreview(request.responseText)
87
+ } else {
88
+ this.showError()
89
+ }
90
+ }
91
+
92
+ validateFields () {
93
+ let allValid = true
94
+ const fields = this.fieldsTarget
95
+ const formElements = fields.querySelectorAll('input[name], select[name], textarea[name]')
96
+ formElements.forEach(function (element) {
97
+ const isValid = element.reportValidity()
98
+ if (!isValid) {
99
+ allValid = false
100
+ }
101
+ })
102
+ return allValid
103
+ }
104
+
105
+ buildFormData () {
106
+ // Get fields
107
+ const fields = this.fieldsTarget
108
+
109
+ // Build FormData
110
+ const formData = new FormData()
111
+
112
+ // Replace all occurrences of "page[blocks_attributes][0]" with "block"
113
+ const regex = /\w+\[([^\]]+)s_attributes\]\[\d+\]/g
114
+ const formElements = fields.querySelectorAll('input[name]:not([name$="[id]"]), select[name]:not([name$="[id]"]), textarea[name]:not([name$="[id]"]), button[name]:not([name$="[id]"])')
115
+ formElements.forEach(function (element) {
116
+ const currentName = element.getAttribute('name')
117
+ const newName = currentName.replace(regex, '$1')
118
+ formData.append(newName, element.value)
119
+ })
120
+
121
+ // Add authenticity token
122
+ formData.append('authenticity_token', this.getAuthenticityToken())
123
+
124
+ return formData
125
+ }
126
+
127
+ // Prepare the iFrame for rendering
128
+ // Objective: render the iframe content at the scale of the browser window, but resize it to fit the preview container
129
+ prepareIframe () {
130
+ const scaleFactor = this.scaleFactor()
131
+ const style = `
132
+ transform: scale(${scaleFactor});
133
+ opacity: 0;
134
+ transform-origin: 0 0;
135
+ width: ${100 / scaleFactor}%;
136
+ `
137
+ this.iframeTarget.setAttribute('style', style)
138
+ }
139
+
140
+ // Relative size of the preview container compared to the browser window
141
+ scaleFactor () {
142
+ const width = this.iframeWrapperTarget.getBoundingClientRect().width
143
+ const viewportWidth = window.innerWidth
144
+ return (width / viewportWidth).toFixed(1)
145
+ }
146
+
147
+ // Replace the body of the iframe with the new content
148
+ updatePreview (html) {
149
+ this.iframeTarget.contentWindow.document.body.innerHTML = html
150
+ this.resizeIframe()
151
+ }
152
+
153
+ // Dynamically resize the iFrame to fit its content
154
+ resizeIframe () {
155
+ const scaleFactor = this.scaleFactor()
156
+ const iframeContentHeight = this.iframeTarget.contentWindow.document.body.scrollHeight
157
+ const iframeHeight = iframeContentHeight * scaleFactor
158
+ this.iframeTarget.style.height = iframeContentHeight + 'px'
159
+ this.iframeTarget.style.opacity = 1
160
+ this.iframeWrapperTarget.style.height = iframeHeight + 'px'
161
+ }
162
+
163
+ getAuthenticityToken () {
164
+ const tokenTag = document.querySelector('meta[name="csrf-token"]')
165
+ return tokenTag.getAttribute('content')
166
+ }
167
+ }
@@ -0,0 +1,65 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller {
4
+ static values = {
5
+ url: String
6
+ }
7
+
8
+ connect () {
9
+ this.button = this.element
10
+ this.button.addEventListener('click', (event) => {
11
+ event.preventDefault()
12
+ this.requestPreview()
13
+ })
14
+ }
15
+
16
+ requestPreview () {
17
+ const form = this.buildFakeForm()
18
+
19
+ // Insert in DOM
20
+ document.body.appendChild(form)
21
+
22
+ // Submit form
23
+ form.submit()
24
+
25
+ // Remove from DOM
26
+ document.body.removeChild(form)
27
+ }
28
+
29
+ buildFakeForm () {
30
+ const form = this.form().cloneNode(true)
31
+
32
+ // Empty [id] fields
33
+ const idInputs = form.querySelectorAll('input[name$="[id]"], select[name$="[id]"], textarea[name$="[id]"], button[name$="[id]"]')
34
+ idInputs.forEach((input) => {
35
+ input.value = ''
36
+ })
37
+
38
+ // Set preview action
39
+ form.setAttribute('action', this.urlValue)
40
+
41
+ // Set target to blank
42
+ form.setAttribute('target', '_blank')
43
+
44
+ // Refresh authenticity token
45
+ const authenticityTokenInput = form.querySelector('input[name="authenticity_token"]')
46
+ authenticityTokenInput.value = this.getAuthenticityToken()
47
+
48
+ // Remove method input if present (to force POST)
49
+ form.querySelector('input[name="_method"]')?.remove()
50
+
51
+ // Ensure POST method
52
+ form.setAttribute('method', 'POST')
53
+
54
+ return form
55
+ }
56
+
57
+ getAuthenticityToken () {
58
+ const tokenTag = document.querySelector('meta[name="csrf-token"]')
59
+ return tokenTag.getAttribute('content')
60
+ }
61
+
62
+ form () {
63
+ return this.button.closest('form')
64
+ }
65
+ }
@@ -39,7 +39,7 @@ export default class extends Controller {
39
39
 
40
40
  updatePopupButtonIndices (index) {
41
41
  const popup = document.querySelector(`[data-popup-target="popup"][data-popup-id="repeater-buttons-${this.idValue}"]`)
42
- const buttons = popup.querySelectorAll('a')
42
+ const buttons = popup.querySelectorAll('[data-popup-target="button"]')
43
43
  buttons.forEach((button) => {
44
44
  button.dataset.rowIndex = index
45
45
  })
@@ -52,17 +52,17 @@ export default class extends Controller {
52
52
  const rowIndex = button.dataset.rowIndex
53
53
 
54
54
  // Prepare html from template
55
- const template = this.getTemplate(templateName)
56
- const html = this.replaceIdsWithTimestamps(template)
55
+ let template = this.getTemplate(templateName).content.cloneNode(true)
56
+ template = this.replaceIdsWithTimestamps(template)
57
57
 
58
58
  // Fallback to last row if no index is set
59
59
  if (rowIndex) {
60
60
  // Insert new row after defined row
61
61
  const row = this.rowTargets[rowIndex]
62
- row.insertAdjacentHTML('afterend', html)
62
+ this.listTarget.insertBefore(template, row.nextSibling)
63
63
  } else {
64
64
  // Insert before footer
65
- this.footerTarget.insertAdjacentHTML('beforebegin', html)
65
+ this.listTarget.insertBefore(template, this.footerTarget)
66
66
  }
67
67
 
68
68
  this.resetIndices()
@@ -108,8 +108,40 @@ export default class extends Controller {
108
108
  }
109
109
 
110
110
  replaceIdsWithTimestamps (template) {
111
- const regex = new RegExp(template.dataset.templateIdRegex, 'g')
112
- return template.innerHTML.replace(regex, new Date().getTime())
111
+ const pattern = 'rrrrrrrrr'
112
+ const replacement = new Date().getTime().toString()
113
+
114
+ // Replace ids
115
+ template.querySelectorAll(`input[id*="${pattern}"], select[id*="${pattern}"], textarea[id*="${pattern}"], button[id*="${pattern}"]`).forEach((node) => {
116
+ const idValue = node.getAttribute('id')
117
+ node.setAttribute('id', idValue.replace(pattern, replacement))
118
+ })
119
+
120
+ // Replace labels
121
+ template.querySelectorAll(`label[for*="${pattern}"]`).forEach((node) => {
122
+ const forValue = node.getAttribute('for')
123
+ node.setAttribute('for', forValue.replace(pattern, replacement))
124
+ })
125
+
126
+ // Replace names
127
+ template.querySelectorAll(`input[name*="${pattern}"], select[name*="${pattern}"], textarea[name*="${pattern}"], button[name*="${pattern}"]`).forEach((node) => {
128
+ const nameValue = node.getAttribute('name')
129
+ node.setAttribute('name', nameValue.replace(pattern, replacement))
130
+ })
131
+
132
+ // Replace offcanvas targets
133
+ template.querySelectorAll(`div[data-bs-target="#offcanvas-${pattern}"]`).forEach((node) => {
134
+ const targetValue = node.getAttribute('data-bs-target')
135
+ node.setAttribute('data-bs-target', targetValue.replace(pattern, replacement))
136
+ })
137
+
138
+ // Replace offcanvas ids
139
+ template.querySelectorAll(`.offcanvas[id="offcanvas-${pattern}"]`).forEach((node) => {
140
+ const idValue = node.getAttribute('id')
141
+ node.setAttribute('id', idValue.replace(pattern, replacement))
142
+ })
143
+
144
+ return template
113
145
  }
114
146
 
115
147
  visibleRowsCount () {
@@ -8,7 +8,9 @@ import FlatpickrController from './controllers/flatpickr_controller'
8
8
  import InfiniteScrollerController from './controllers/infinite_scroller_controller'
9
9
  import MediaController from './controllers/media_controller'
10
10
  import MediaModalController from './controllers/media_modal_controller'
11
+ import NestedPreviewController from './controllers/nested_preview_controller'
11
12
  import PopupController from './controllers/popup_controller'
13
+ import PreviewController from './controllers/preview_controller'
12
14
  import RedactorxController from './controllers/redactorx_controller'
13
15
  import RepeaterController from './controllers/repeater_controller'
14
16
  import SelectController from './controllers/select_controller'
@@ -25,7 +27,9 @@ export class Formstrap {
25
27
  Stimulus.register('infinite-scroller', InfiniteScrollerController)
26
28
  Stimulus.register('media', MediaController)
27
29
  Stimulus.register('media-modal', MediaModalController)
30
+ Stimulus.register('nested-preview', NestedPreviewController)
28
31
  Stimulus.register('popup', PopupController)
32
+ Stimulus.register('preview', PreviewController)
29
33
  Stimulus.register('redactorx', RedactorxController)
30
34
  Stimulus.register('repeater', RepeaterController)
31
35
  Stimulus.register('select', SelectController)
@@ -4,6 +4,7 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __getProtoOf = Object.getPrototypeOf;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
7
8
  var __commonJS = (cb, mod) => function __require() {
8
9
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
10
  };
@@ -19,6 +20,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
19
20
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
20
21
  mod
21
22
  ));
23
+ var __publicField = (obj, key, value) => {
24
+ __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
25
+ return value;
26
+ };
22
27
 
23
28
  // node_modules/tom-select/dist/js/tom-select.complete.js
24
29
  var require_tom_select_complete = __commonJS({
@@ -11288,6 +11293,129 @@ var media_modal_controller_default = class extends Controller {
11288
11293
  }
11289
11294
  };
11290
11295
 
11296
+ // app/assets/javascripts/formstrap/controllers/nested_preview_controller.js
11297
+ var nested_preview_controller_default = class extends Controller {
11298
+ static get targets() {
11299
+ return ["fields", "iframeWrapper", "iframe", "offcanvas", "error", "loader"];
11300
+ }
11301
+ static get values() {
11302
+ return {
11303
+ url: String
11304
+ };
11305
+ }
11306
+ connect() {
11307
+ this.prepareIframe();
11308
+ this.iframeTarget.addEventListener("load", () => {
11309
+ this.hideLoader();
11310
+ this.resizeIframe();
11311
+ });
11312
+ this.offcanvasTarget.addEventListener("hide.bs.offcanvas", (event) => {
11313
+ if (!this.update()) {
11314
+ event.preventDefault();
11315
+ }
11316
+ });
11317
+ }
11318
+ showLoader() {
11319
+ this.loaderTarget.classList.remove("d-none");
11320
+ }
11321
+ hideLoader() {
11322
+ this.loaderTarget.classList.add("d-none");
11323
+ }
11324
+ showError() {
11325
+ this.errorTarget.classList.remove("d-none");
11326
+ }
11327
+ hideError() {
11328
+ this.errorTarget.classList.add("d-none");
11329
+ }
11330
+ update() {
11331
+ const isValid = this.validateFields();
11332
+ if (isValid) {
11333
+ this.requestPreview();
11334
+ this.hideError();
11335
+ return true;
11336
+ } else {
11337
+ this.showError();
11338
+ return false;
11339
+ }
11340
+ }
11341
+ requestPreview() {
11342
+ const xhr = new XMLHttpRequest();
11343
+ xhr.open("POST", this.urlValue, true);
11344
+ const formData = this.buildFormData();
11345
+ xhr.send(formData);
11346
+ this.showLoader();
11347
+ xhr.onreadystatechange = () => {
11348
+ if (xhr.readyState === XMLHttpRequest.DONE) {
11349
+ this.hideLoader();
11350
+ this.handleRequest(xhr);
11351
+ }
11352
+ };
11353
+ }
11354
+ handleRequest(request) {
11355
+ if (request.status === 200) {
11356
+ this.updatePreview(request.responseText);
11357
+ } else {
11358
+ this.showError();
11359
+ }
11360
+ }
11361
+ validateFields() {
11362
+ let allValid = true;
11363
+ const fields = this.fieldsTarget;
11364
+ const formElements = fields.querySelectorAll("input[name], select[name], textarea[name]");
11365
+ formElements.forEach(function(element) {
11366
+ const isValid = element.reportValidity();
11367
+ if (!isValid) {
11368
+ allValid = false;
11369
+ }
11370
+ });
11371
+ return allValid;
11372
+ }
11373
+ buildFormData() {
11374
+ const fields = this.fieldsTarget;
11375
+ const formData = new FormData();
11376
+ const regex = /\w+\[([^\]]+)s_attributes\]\[\d+\]/g;
11377
+ const formElements = fields.querySelectorAll('input[name]:not([name$="[id]"]), select[name]:not([name$="[id]"]), textarea[name]:not([name$="[id]"]), button[name]:not([name$="[id]"])');
11378
+ formElements.forEach(function(element) {
11379
+ const currentName = element.getAttribute("name");
11380
+ const newName = currentName.replace(regex, "$1");
11381
+ formData.append(newName, element.value);
11382
+ });
11383
+ formData.append("authenticity_token", this.getAuthenticityToken());
11384
+ return formData;
11385
+ }
11386
+ prepareIframe() {
11387
+ const scaleFactor = this.scaleFactor();
11388
+ const style = `
11389
+ transform: scale(${scaleFactor});
11390
+ opacity: 0;
11391
+ transform-origin: 0 0;
11392
+ width: ${100 / scaleFactor}%;
11393
+ `;
11394
+ this.iframeTarget.setAttribute("style", style);
11395
+ }
11396
+ scaleFactor() {
11397
+ const width = this.iframeWrapperTarget.getBoundingClientRect().width;
11398
+ const viewportWidth = window.innerWidth;
11399
+ return (width / viewportWidth).toFixed(1);
11400
+ }
11401
+ updatePreview(html) {
11402
+ this.iframeTarget.contentWindow.document.body.innerHTML = html;
11403
+ this.resizeIframe();
11404
+ }
11405
+ resizeIframe() {
11406
+ const scaleFactor = this.scaleFactor();
11407
+ const iframeContentHeight = this.iframeTarget.contentWindow.document.body.scrollHeight;
11408
+ const iframeHeight = iframeContentHeight * scaleFactor;
11409
+ this.iframeTarget.style.height = iframeContentHeight + "px";
11410
+ this.iframeTarget.style.opacity = 1;
11411
+ this.iframeWrapperTarget.style.height = iframeHeight + "px";
11412
+ }
11413
+ getAuthenticityToken() {
11414
+ const tokenTag = document.querySelector('meta[name="csrf-token"]');
11415
+ return tokenTag.getAttribute("content");
11416
+ }
11417
+ };
11418
+
11291
11419
  // node_modules/@popperjs/core/lib/enums.js
11292
11420
  var top = "top";
11293
11421
  var bottom = "bottom";
@@ -12917,6 +13045,47 @@ var popup_controller_default = class extends Controller {
12917
13045
  }
12918
13046
  };
12919
13047
 
13048
+ // app/assets/javascripts/formstrap/controllers/preview_controller.js
13049
+ var preview_controller_default = class extends Controller {
13050
+ connect() {
13051
+ this.button = this.element;
13052
+ this.button.addEventListener("click", (event) => {
13053
+ event.preventDefault();
13054
+ this.requestPreview();
13055
+ });
13056
+ }
13057
+ requestPreview() {
13058
+ const form = this.buildFakeForm();
13059
+ document.body.appendChild(form);
13060
+ form.submit();
13061
+ document.body.removeChild(form);
13062
+ }
13063
+ buildFakeForm() {
13064
+ const form = this.form().cloneNode(true);
13065
+ const idInputs = form.querySelectorAll('input[name$="[id]"], select[name$="[id]"], textarea[name$="[id]"], button[name$="[id]"]');
13066
+ idInputs.forEach((input) => {
13067
+ input.value = "";
13068
+ });
13069
+ form.setAttribute("action", this.urlValue);
13070
+ form.setAttribute("target", "_blank");
13071
+ const authenticityTokenInput = form.querySelector('input[name="authenticity_token"]');
13072
+ authenticityTokenInput.value = this.getAuthenticityToken();
13073
+ form.querySelector('input[name="_method"]')?.remove();
13074
+ form.setAttribute("method", "POST");
13075
+ return form;
13076
+ }
13077
+ getAuthenticityToken() {
13078
+ const tokenTag = document.querySelector('meta[name="csrf-token"]');
13079
+ return tokenTag.getAttribute("content");
13080
+ }
13081
+ form() {
13082
+ return this.button.closest("form");
13083
+ }
13084
+ };
13085
+ __publicField(preview_controller_default, "values", {
13086
+ url: String
13087
+ });
13088
+
12920
13089
  // app/assets/javascripts/formstrap/controllers/redactorx_controller.js
12921
13090
  var redactorx_controller_default = class extends Controller {
12922
13091
  connect() {
@@ -12980,7 +13149,7 @@ var repeater_controller_default = class extends Controller {
12980
13149
  }
12981
13150
  updatePopupButtonIndices(index2) {
12982
13151
  const popup = document.querySelector(`[data-popup-target="popup"][data-popup-id="repeater-buttons-${this.idValue}"]`);
12983
- const buttons = popup.querySelectorAll("a");
13152
+ const buttons = popup.querySelectorAll('[data-popup-target="button"]');
12984
13153
  buttons.forEach((button) => {
12985
13154
  button.dataset.rowIndex = index2;
12986
13155
  });
@@ -12990,13 +13159,13 @@ var repeater_controller_default = class extends Controller {
12990
13159
  const button = event.target;
12991
13160
  const templateName = button.dataset.templateName;
12992
13161
  const rowIndex = button.dataset.rowIndex;
12993
- const template = this.getTemplate(templateName);
12994
- const html = this.replaceIdsWithTimestamps(template);
13162
+ let template = this.getTemplate(templateName).content.cloneNode(true);
13163
+ template = this.replaceIdsWithTimestamps(template);
12995
13164
  if (rowIndex) {
12996
13165
  const row = this.rowTargets[rowIndex];
12997
- row.insertAdjacentHTML("afterend", html);
13166
+ this.listTarget.insertBefore(template, row.nextSibling);
12998
13167
  } else {
12999
- this.footerTarget.insertAdjacentHTML("beforebegin", html);
13168
+ this.listTarget.insertBefore(template, this.footerTarget);
13000
13169
  }
13001
13170
  this.resetIndices();
13002
13171
  this.resetPositions();
@@ -13028,8 +13197,29 @@ var repeater_controller_default = class extends Controller {
13028
13197
  })[0];
13029
13198
  }
13030
13199
  replaceIdsWithTimestamps(template) {
13031
- const regex = new RegExp(template.dataset.templateIdRegex, "g");
13032
- return template.innerHTML.replace(regex, new Date().getTime());
13200
+ const pattern = "rrrrrrrrr";
13201
+ const replacement = new Date().getTime().toString();
13202
+ template.querySelectorAll(`input[id*="${pattern}"], select[id*="${pattern}"], textarea[id*="${pattern}"], button[id*="${pattern}"]`).forEach((node) => {
13203
+ const idValue = node.getAttribute("id");
13204
+ node.setAttribute("id", idValue.replace(pattern, replacement));
13205
+ });
13206
+ template.querySelectorAll(`label[for*="${pattern}"]`).forEach((node) => {
13207
+ const forValue = node.getAttribute("for");
13208
+ node.setAttribute("for", forValue.replace(pattern, replacement));
13209
+ });
13210
+ template.querySelectorAll(`input[name*="${pattern}"], select[name*="${pattern}"], textarea[name*="${pattern}"], button[name*="${pattern}"]`).forEach((node) => {
13211
+ const nameValue = node.getAttribute("name");
13212
+ node.setAttribute("name", nameValue.replace(pattern, replacement));
13213
+ });
13214
+ template.querySelectorAll(`div[data-bs-target="#offcanvas-${pattern}"]`).forEach((node) => {
13215
+ const targetValue = node.getAttribute("data-bs-target");
13216
+ node.setAttribute("data-bs-target", targetValue.replace(pattern, replacement));
13217
+ });
13218
+ template.querySelectorAll(`.offcanvas[id="offcanvas-${pattern}"]`).forEach((node) => {
13219
+ const idValue = node.getAttribute("id");
13220
+ node.setAttribute("id", idValue.replace(pattern, replacement));
13221
+ });
13222
+ return template;
13033
13223
  }
13034
13224
  visibleRowsCount() {
13035
13225
  return this.visibleRows().length;
@@ -13157,7 +13347,9 @@ var Formstrap = class {
13157
13347
  Stimulus.register("infinite-scroller", infinite_scroller_controller_default);
13158
13348
  Stimulus.register("media", media_controller_default);
13159
13349
  Stimulus.register("media-modal", media_modal_controller_default);
13350
+ Stimulus.register("nested-preview", nested_preview_controller_default);
13160
13351
  Stimulus.register("popup", popup_controller_default);
13352
+ Stimulus.register("preview", preview_controller_default);
13161
13353
  Stimulus.register("redactorx", redactorx_controller_default);
13162
13354
  Stimulus.register("repeater", repeater_controller_default);
13163
13355
  Stimulus.register("select", select_controller_default);
@@ -0,0 +1,31 @@
1
+ .nested-preview-offcanvas, .nested-preview .hiding {
2
+ display: none !important;
3
+
4
+ &.show, &.showing {
5
+ display: flex !important;
6
+ }
7
+ }
8
+
9
+ .nested-preview-iframe-wrapper {
10
+ position: relative;
11
+ cursor: pointer;
12
+
13
+ iframe {
14
+ width: 100%;
15
+ }
16
+ }
17
+
18
+ .nested-preview-loader {
19
+ position: absolute;
20
+ width: 100%;
21
+ height: 100%;
22
+ z-index: 2;
23
+ top: 0;
24
+ left: 0;
25
+ padding: 40px;
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ background: var(--bs-secondary-bg);
30
+ opacity: 0.5;
31
+ }
@@ -1,2 +1,3 @@
1
1
  @import "shared/thumbnail";
2
2
  @import "shared/popup";
3
+ @import "shared/nested_preview";
@@ -1400,6 +1400,37 @@ mark {
1400
1400
  .formstrap-popup.closed {
1401
1401
  display: none;
1402
1402
  }
1403
+ .nested-preview-offcanvas,
1404
+ .nested-preview .hiding {
1405
+ display: none !important;
1406
+ }
1407
+ .nested-preview-offcanvas.show,
1408
+ .nested-preview-offcanvas.showing,
1409
+ .nested-preview .hiding.show,
1410
+ .nested-preview .hiding.showing {
1411
+ display: flex !important;
1412
+ }
1413
+ .nested-preview-iframe-wrapper {
1414
+ position: relative;
1415
+ cursor: pointer;
1416
+ }
1417
+ .nested-preview-iframe-wrapper iframe {
1418
+ width: 100%;
1419
+ }
1420
+ .nested-preview-loader {
1421
+ position: absolute;
1422
+ width: 100%;
1423
+ height: 100%;
1424
+ z-index: 2;
1425
+ top: 0;
1426
+ left: 0;
1427
+ padding: 40px;
1428
+ display: flex;
1429
+ align-items: center;
1430
+ justify-content: center;
1431
+ background: var(--bs-secondary-bg);
1432
+ opacity: 0.5;
1433
+ }
1403
1434
  .formstrap-media-modal .formstrap-thumbnail {
1404
1435
  cursor: pointer;
1405
1436
  }
File without changes