formstrap 0.2.1 → 0.3.1

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 (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
+ }