formstrap 0.2.1 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/app/assets/javascripts/formstrap/controllers/nested_preview_controller.js +194 -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 +219 -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. data/package.json +1 -1
  26. metadata +13 -8
  27. data/config/locales/formstrap/forms/en.yml +0 -25
  28. data/config/locales/formstrap/forms/nl.yml +0 -25
  29. data/config/locales/formstrap/media/en.yml +0 -16
  30. data/config/locales/formstrap/media/nl.yml +0 -16
  31. data/config/locales/formstrap/thumbnail/en.yml +0 -4
  32. 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: 2d52dd192e41d84f75bc6be46186bd8a5739ab34f72b8a03a7f57a2518024356
4
+ data.tar.gz: 21985d08ac6f4837e06385a0de891e0785366322f8504a537f22a89d397903aa
5
5
  SHA512:
6
- metadata.gz: 4bee48dada36d1789b7d913af8e22a4805bf2ffff7883f3e9a280d28799556abd9525045ff7395165255dfa84f5782bbde9c29d9855f61e8677b4d8cfa89120b
7
- data.tar.gz: 8c36be158e08f4fdfa85e990f5e74c0cf5e355a172303e7eb2c65214b13267ab5882ac0d873c77b2cb119f01e761c47a0761bd9b97f7d0a47a9d192d733e478f
6
+ metadata.gz: d4e86b30d1f485050c98c72ef426d3590624544e86675b41efe409382595d8f121878b437cf5fdb7ff8dc7aef71dd11c5f689b3bae73d805d1011f6c68cd965b
7
+ data.tar.gz: 1f04bdc357480984893120ef11166a2c62ade2e80ad793d49814a550f5e35099fd559ca05f57070ce51a166691a1c4c9a916fedd357b29f38c41b563e3c15601
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,194 @@
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
+ this.autoResizeIframe()
22
+ })
23
+
24
+ // Offcanvas closes
25
+ this.offcanvasTarget.addEventListener('hide.bs.offcanvas', (event) => {
26
+ if (!this.update()) {
27
+ event.preventDefault()
28
+ }
29
+ })
30
+ }
31
+
32
+ autoResizeIframe () {
33
+ // eslint-disable-next-line no-undef
34
+ const observer = new MutationObserver((mutations) => {
35
+ mutations.forEach((mutation) => {
36
+ this.resizeIframe()
37
+ })
38
+ })
39
+
40
+ // Target the body of the iframe for observing
41
+ const innerDoc = this.iframeTarget.contentWindow.document
42
+ observer.observe(innerDoc.body, {
43
+ childList: true, // Listen for additions/removals of child nodes
44
+ subtree: true // Listen for changes in the whole subtree
45
+ })
46
+ }
47
+
48
+ showLoader () {
49
+ this.loaderTarget.classList.remove('d-none')
50
+ }
51
+
52
+ hideLoader () {
53
+ this.loaderTarget.classList.add('d-none')
54
+ }
55
+
56
+ showError () {
57
+ this.errorTarget.classList.remove('d-none')
58
+ }
59
+
60
+ hideError () {
61
+ this.errorTarget.classList.add('d-none')
62
+ }
63
+
64
+ update () {
65
+ // Validate fields
66
+ const isValid = this.validateFields()
67
+
68
+ if (isValid) {
69
+ this.requestPreview()
70
+ this.hideError()
71
+ return true
72
+ } else {
73
+ this.showError()
74
+ return false
75
+ }
76
+ }
77
+
78
+ requestPreview () {
79
+ // Create an AJAX request
80
+ // eslint-disable-next-line no-undef
81
+ const xhr = new XMLHttpRequest()
82
+ xhr.open('POST', this.urlValue, true)
83
+
84
+ // Submit the form data
85
+ const formData = this.buildFormData()
86
+ xhr.send(formData)
87
+
88
+ // Show loader
89
+ this.showLoader()
90
+
91
+ // Handle the request once it's done
92
+ xhr.onreadystatechange = () => {
93
+ if (xhr.readyState === XMLHttpRequest.DONE) {
94
+ this.hideLoader()
95
+ this.handleRequest(xhr)
96
+ }
97
+ }
98
+ }
99
+
100
+ handleRequest (request) {
101
+ // Handle the response
102
+ if (request.status === 200) {
103
+ this.updatePreview(request.responseText)
104
+ } else {
105
+ this.showError()
106
+ }
107
+ }
108
+
109
+ validateFields () {
110
+ let allValid = true
111
+ const fields = this.fieldsTarget
112
+ const formElements = fields.querySelectorAll('input[name], select[name], textarea[name]')
113
+ formElements.forEach(function (element) {
114
+ const isValid = element.reportValidity()
115
+ if (!isValid) {
116
+ allValid = false
117
+ }
118
+ })
119
+ return allValid
120
+ }
121
+
122
+ buildFormData () {
123
+ // Get fields
124
+ const fields = this.fieldsTarget
125
+
126
+ // Build FormData
127
+ const formData = new FormData()
128
+
129
+ // Replace all occurrences of "page[blocks_attributes][0]" with "block"
130
+ const regex = /\w+\[([^\]]+)s_attributes\]\[\d+\]/g
131
+ const formElements = fields.querySelectorAll('input[name]:not([name$="[id]"]), select[name]:not([name$="[id]"]), textarea[name]:not([name$="[id]"]), button[name]:not([name$="[id]"])')
132
+ formElements.forEach(function (element) {
133
+ const currentName = element.getAttribute('name')
134
+ const newName = currentName.replace(regex, '$1')
135
+ formData.append(newName, element.value)
136
+ })
137
+
138
+ // Add authenticity token
139
+ formData.append('authenticity_token', this.getAuthenticityToken())
140
+
141
+ return formData
142
+ }
143
+
144
+ // Prepare the iFrame for rendering
145
+ // Objective: render the iframe content at the scale of the browser window, but resize it to fit the preview container
146
+ prepareIframe () {
147
+ const scaleFactor = this.scaleFactor()
148
+ const style = `
149
+ transform: scale(${scaleFactor});
150
+ opacity: 0;
151
+ transform-origin: 0 0;
152
+ width: ${100 / scaleFactor}%;
153
+ `
154
+ this.iframeTarget.setAttribute('style', style)
155
+ }
156
+
157
+ // Relative size of the preview container compared to the browser window
158
+ scaleFactor () {
159
+ const width = this.iframeWrapperTarget.getBoundingClientRect().width
160
+ const viewportWidth = window.innerWidth
161
+ return parseFloat((width / viewportWidth).toFixed(1))
162
+ }
163
+
164
+ // Replace the body of the iframe with the new content
165
+ updatePreview (html) {
166
+ this.iframeTarget.contentWindow.document.body.innerHTML = html
167
+ this.resizeIframe()
168
+ }
169
+
170
+ // Dynamically resize the iFrame to fit its content
171
+ resizeIframe () {
172
+ const scaleFactor = this.scaleFactor()
173
+ const iframeContentHeight = this.iFrameContentHeight()
174
+ const iframeHeight = iframeContentHeight * scaleFactor
175
+
176
+ this.iframeTarget.style.height = `${iframeContentHeight.toFixed()}px`
177
+ this.iframeTarget.style.opacity = 1
178
+ this.iframeWrapperTarget.style.height = `${iframeHeight.toFixed()}px`
179
+ }
180
+
181
+ iFrameContentHeight () {
182
+ const firstElement = this.iframeTarget.contentWindow.document.body.firstElementChild
183
+ const firstElementStyle = window.getComputedStyle(firstElement)
184
+
185
+ const height = firstElement.scrollHeight
186
+ const margins = parseInt(firstElementStyle.marginTop) + parseInt(firstElementStyle.marginBottom)
187
+ return height + margins
188
+ }
189
+
190
+ getAuthenticityToken () {
191
+ const tokenTag = document.querySelector('meta[name="csrf-token"]')
192
+ return tokenTag.getAttribute('content')
193
+ }
194
+ }
@@ -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,149 @@ 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
+ this.autoResizeIframe();
11312
+ });
11313
+ this.offcanvasTarget.addEventListener("hide.bs.offcanvas", (event) => {
11314
+ if (!this.update()) {
11315
+ event.preventDefault();
11316
+ }
11317
+ });
11318
+ }
11319
+ autoResizeIframe() {
11320
+ const observer = new MutationObserver((mutations) => {
11321
+ mutations.forEach((mutation) => {
11322
+ this.resizeIframe();
11323
+ });
11324
+ });
11325
+ const innerDoc = this.iframeTarget.contentWindow.document;
11326
+ observer.observe(innerDoc.body, {
11327
+ childList: true,
11328
+ subtree: true
11329
+ });
11330
+ }
11331
+ showLoader() {
11332
+ this.loaderTarget.classList.remove("d-none");
11333
+ }
11334
+ hideLoader() {
11335
+ this.loaderTarget.classList.add("d-none");
11336
+ }
11337
+ showError() {
11338
+ this.errorTarget.classList.remove("d-none");
11339
+ }
11340
+ hideError() {
11341
+ this.errorTarget.classList.add("d-none");
11342
+ }
11343
+ update() {
11344
+ const isValid = this.validateFields();
11345
+ if (isValid) {
11346
+ this.requestPreview();
11347
+ this.hideError();
11348
+ return true;
11349
+ } else {
11350
+ this.showError();
11351
+ return false;
11352
+ }
11353
+ }
11354
+ requestPreview() {
11355
+ const xhr = new XMLHttpRequest();
11356
+ xhr.open("POST", this.urlValue, true);
11357
+ const formData = this.buildFormData();
11358
+ xhr.send(formData);
11359
+ this.showLoader();
11360
+ xhr.onreadystatechange = () => {
11361
+ if (xhr.readyState === XMLHttpRequest.DONE) {
11362
+ this.hideLoader();
11363
+ this.handleRequest(xhr);
11364
+ }
11365
+ };
11366
+ }
11367
+ handleRequest(request) {
11368
+ if (request.status === 200) {
11369
+ this.updatePreview(request.responseText);
11370
+ } else {
11371
+ this.showError();
11372
+ }
11373
+ }
11374
+ validateFields() {
11375
+ let allValid = true;
11376
+ const fields = this.fieldsTarget;
11377
+ const formElements = fields.querySelectorAll("input[name], select[name], textarea[name]");
11378
+ formElements.forEach(function(element) {
11379
+ const isValid = element.reportValidity();
11380
+ if (!isValid) {
11381
+ allValid = false;
11382
+ }
11383
+ });
11384
+ return allValid;
11385
+ }
11386
+ buildFormData() {
11387
+ const fields = this.fieldsTarget;
11388
+ const formData = new FormData();
11389
+ const regex = /\w+\[([^\]]+)s_attributes\]\[\d+\]/g;
11390
+ const formElements = fields.querySelectorAll('input[name]:not([name$="[id]"]), select[name]:not([name$="[id]"]), textarea[name]:not([name$="[id]"]), button[name]:not([name$="[id]"])');
11391
+ formElements.forEach(function(element) {
11392
+ const currentName = element.getAttribute("name");
11393
+ const newName = currentName.replace(regex, "$1");
11394
+ formData.append(newName, element.value);
11395
+ });
11396
+ formData.append("authenticity_token", this.getAuthenticityToken());
11397
+ return formData;
11398
+ }
11399
+ prepareIframe() {
11400
+ const scaleFactor = this.scaleFactor();
11401
+ const style = `
11402
+ transform: scale(${scaleFactor});
11403
+ opacity: 0;
11404
+ transform-origin: 0 0;
11405
+ width: ${100 / scaleFactor}%;
11406
+ `;
11407
+ this.iframeTarget.setAttribute("style", style);
11408
+ }
11409
+ scaleFactor() {
11410
+ const width = this.iframeWrapperTarget.getBoundingClientRect().width;
11411
+ const viewportWidth = window.innerWidth;
11412
+ return parseFloat((width / viewportWidth).toFixed(1));
11413
+ }
11414
+ updatePreview(html) {
11415
+ this.iframeTarget.contentWindow.document.body.innerHTML = html;
11416
+ this.resizeIframe();
11417
+ }
11418
+ resizeIframe() {
11419
+ const scaleFactor = this.scaleFactor();
11420
+ const iframeContentHeight = this.iFrameContentHeight();
11421
+ const iframeHeight = iframeContentHeight * scaleFactor;
11422
+ this.iframeTarget.style.height = `${iframeContentHeight.toFixed()}px`;
11423
+ this.iframeTarget.style.opacity = 1;
11424
+ this.iframeWrapperTarget.style.height = `${iframeHeight.toFixed()}px`;
11425
+ }
11426
+ iFrameContentHeight() {
11427
+ const firstElement = this.iframeTarget.contentWindow.document.body.firstElementChild;
11428
+ const firstElementStyle = window.getComputedStyle(firstElement);
11429
+ const height = firstElement.scrollHeight;
11430
+ const margins = parseInt(firstElementStyle.marginTop) + parseInt(firstElementStyle.marginBottom);
11431
+ return height + margins;
11432
+ }
11433
+ getAuthenticityToken() {
11434
+ const tokenTag = document.querySelector('meta[name="csrf-token"]');
11435
+ return tokenTag.getAttribute("content");
11436
+ }
11437
+ };
11438
+
11291
11439
  // node_modules/@popperjs/core/lib/enums.js
11292
11440
  var top = "top";
11293
11441
  var bottom = "bottom";
@@ -12917,6 +13065,47 @@ var popup_controller_default = class extends Controller {
12917
13065
  }
12918
13066
  };
12919
13067
 
13068
+ // app/assets/javascripts/formstrap/controllers/preview_controller.js
13069
+ var preview_controller_default = class extends Controller {
13070
+ connect() {
13071
+ this.button = this.element;
13072
+ this.button.addEventListener("click", (event) => {
13073
+ event.preventDefault();
13074
+ this.requestPreview();
13075
+ });
13076
+ }
13077
+ requestPreview() {
13078
+ const form = this.buildFakeForm();
13079
+ document.body.appendChild(form);
13080
+ form.submit();
13081
+ document.body.removeChild(form);
13082
+ }
13083
+ buildFakeForm() {
13084
+ const form = this.form().cloneNode(true);
13085
+ const idInputs = form.querySelectorAll('input[name$="[id]"], select[name$="[id]"], textarea[name$="[id]"], button[name$="[id]"]');
13086
+ idInputs.forEach((input) => {
13087
+ input.value = "";
13088
+ });
13089
+ form.setAttribute("action", this.urlValue);
13090
+ form.setAttribute("target", "_blank");
13091
+ const authenticityTokenInput = form.querySelector('input[name="authenticity_token"]');
13092
+ authenticityTokenInput.value = this.getAuthenticityToken();
13093
+ form.querySelector('input[name="_method"]')?.remove();
13094
+ form.setAttribute("method", "POST");
13095
+ return form;
13096
+ }
13097
+ getAuthenticityToken() {
13098
+ const tokenTag = document.querySelector('meta[name="csrf-token"]');
13099
+ return tokenTag.getAttribute("content");
13100
+ }
13101
+ form() {
13102
+ return this.button.closest("form");
13103
+ }
13104
+ };
13105
+ __publicField(preview_controller_default, "values", {
13106
+ url: String
13107
+ });
13108
+
12920
13109
  // app/assets/javascripts/formstrap/controllers/redactorx_controller.js
12921
13110
  var redactorx_controller_default = class extends Controller {
12922
13111
  connect() {
@@ -12980,7 +13169,7 @@ var repeater_controller_default = class extends Controller {
12980
13169
  }
12981
13170
  updatePopupButtonIndices(index2) {
12982
13171
  const popup = document.querySelector(`[data-popup-target="popup"][data-popup-id="repeater-buttons-${this.idValue}"]`);
12983
- const buttons = popup.querySelectorAll("a");
13172
+ const buttons = popup.querySelectorAll('[data-popup-target="button"]');
12984
13173
  buttons.forEach((button) => {
12985
13174
  button.dataset.rowIndex = index2;
12986
13175
  });
@@ -12990,13 +13179,13 @@ var repeater_controller_default = class extends Controller {
12990
13179
  const button = event.target;
12991
13180
  const templateName = button.dataset.templateName;
12992
13181
  const rowIndex = button.dataset.rowIndex;
12993
- const template = this.getTemplate(templateName);
12994
- const html = this.replaceIdsWithTimestamps(template);
13182
+ let template = this.getTemplate(templateName).content.cloneNode(true);
13183
+ template = this.replaceIdsWithTimestamps(template);
12995
13184
  if (rowIndex) {
12996
13185
  const row = this.rowTargets[rowIndex];
12997
- row.insertAdjacentHTML("afterend", html);
13186
+ this.listTarget.insertBefore(template, row.nextSibling);
12998
13187
  } else {
12999
- this.footerTarget.insertAdjacentHTML("beforebegin", html);
13188
+ this.listTarget.insertBefore(template, this.footerTarget);
13000
13189
  }
13001
13190
  this.resetIndices();
13002
13191
  this.resetPositions();
@@ -13028,8 +13217,29 @@ var repeater_controller_default = class extends Controller {
13028
13217
  })[0];
13029
13218
  }
13030
13219
  replaceIdsWithTimestamps(template) {
13031
- const regex = new RegExp(template.dataset.templateIdRegex, "g");
13032
- return template.innerHTML.replace(regex, new Date().getTime());
13220
+ const pattern = "rrrrrrrrr";
13221
+ const replacement = new Date().getTime().toString();
13222
+ template.querySelectorAll(`input[id*="${pattern}"], select[id*="${pattern}"], textarea[id*="${pattern}"], button[id*="${pattern}"]`).forEach((node) => {
13223
+ const idValue = node.getAttribute("id");
13224
+ node.setAttribute("id", idValue.replace(pattern, replacement));
13225
+ });
13226
+ template.querySelectorAll(`label[for*="${pattern}"]`).forEach((node) => {
13227
+ const forValue = node.getAttribute("for");
13228
+ node.setAttribute("for", forValue.replace(pattern, replacement));
13229
+ });
13230
+ template.querySelectorAll(`input[name*="${pattern}"], select[name*="${pattern}"], textarea[name*="${pattern}"], button[name*="${pattern}"]`).forEach((node) => {
13231
+ const nameValue = node.getAttribute("name");
13232
+ node.setAttribute("name", nameValue.replace(pattern, replacement));
13233
+ });
13234
+ template.querySelectorAll(`div[data-bs-target="#offcanvas-${pattern}"]`).forEach((node) => {
13235
+ const targetValue = node.getAttribute("data-bs-target");
13236
+ node.setAttribute("data-bs-target", targetValue.replace(pattern, replacement));
13237
+ });
13238
+ template.querySelectorAll(`.offcanvas[id="offcanvas-${pattern}"]`).forEach((node) => {
13239
+ const idValue = node.getAttribute("id");
13240
+ node.setAttribute("id", idValue.replace(pattern, replacement));
13241
+ });
13242
+ return template;
13033
13243
  }
13034
13244
  visibleRowsCount() {
13035
13245
  return this.visibleRows().length;
@@ -13157,7 +13367,9 @@ var Formstrap = class {
13157
13367
  Stimulus.register("infinite-scroller", infinite_scroller_controller_default);
13158
13368
  Stimulus.register("media", media_controller_default);
13159
13369
  Stimulus.register("media-modal", media_modal_controller_default);
13370
+ Stimulus.register("nested-preview", nested_preview_controller_default);
13160
13371
  Stimulus.register("popup", popup_controller_default);
13372
+ Stimulus.register("preview", preview_controller_default);
13161
13373
  Stimulus.register("redactorx", redactorx_controller_default);
13162
13374
  Stimulus.register("repeater", repeater_controller_default);
13163
13375
  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
+ }