formstrap 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) 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/popup_controller.js +2 -2
  5. data/app/assets/javascripts/formstrap/controllers/preview_controller.js +65 -0
  6. data/app/assets/javascripts/formstrap/controllers/repeater_controller.js +39 -7
  7. data/app/assets/javascripts/formstrap/index.js +4 -0
  8. data/app/assets/javascripts/formstrap.js +199 -7
  9. data/app/assets/stylesheets/formstrap/shared/nested_preview.scss +31 -0
  10. data/app/assets/stylesheets/formstrap/shared.scss +1 -0
  11. data/app/assets/stylesheets/formstrap.css +31 -0
  12. data/app/models/formstrap/media_view.rb +1 -1
  13. data/app/views/formstrap/_button.html.erb +0 -0
  14. data/app/views/formstrap/_repeater.html.erb +40 -22
  15. data/app/views/formstrap/repeater/_row.html.erb +20 -4
  16. data/app/views/formstrap/shared/_nested_preview.html.erb +35 -0
  17. data/config/locales/de.yml +5 -0
  18. data/config/locales/en.yml +1 -5
  19. data/config/locales/formstrap/de.yml +48 -0
  20. data/config/locales/formstrap/en.yml +48 -0
  21. data/config/locales/formstrap/fr.yml +48 -0
  22. data/config/locales/formstrap/nl.yml +48 -0
  23. data/config/locales/fr.yml +5 -0
  24. data/config/locales/nl.yml +1 -5
  25. data/lib/formstrap/form_builder.rb +15 -0
  26. data/lib/formstrap/version.rb +1 -1
  27. data/package.json +1 -1
  28. metadata +13 -8
  29. data/config/locales/formstrap/forms/en.yml +0 -25
  30. data/config/locales/formstrap/forms/nl.yml +0 -25
  31. data/config/locales/formstrap/media/en.yml +0 -16
  32. data/config/locales/formstrap/media/nl.yml +0 -16
  33. data/config/locales/formstrap/thumbnail/en.yml +0 -4
  34. data/config/locales/formstrap/thumbnail/nl.yml +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 07cc257d4684759a070f1d259212a1e0c10a0edf4f0c9f9dd275b5ef0a3708be
4
- data.tar.gz: 626e3625647a1d0e897f03e3c4ed712eb847e20761a112cc8b3dff667d82d072
3
+ metadata.gz: 8243dd5f8e2fdae1537ffab42b56734ebdfe53e054feb92c68a323954c0a63e0
4
+ data.tar.gz: 0c5a4a73c1d4c518c2ee71cf36f3ccc882ee106a825d94aef212a639ee0bcb94
5
5
  SHA512:
6
- metadata.gz: 16262a9223b70aeb5f6e5ae830e124fd329226fd794c070e2b8afb5ebe1ce2f260c944da356fd8bae5246c1f931bc1b09652dab85f1c9ed8a16335b55978a678
7
- data.tar.gz: 4d0d78db261a85d8cba1dd449b24f206ecc0aac2b3f3287e25bdc4f6c14b57a764bb17322ec7d8523279ee31135cf20fe0aa312182e894398135243a57afe1e4
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
+ }
@@ -1,6 +1,6 @@
1
1
  /* global HTMLInputElement */
2
- import {Controller} from '@hotwired/stimulus'
3
- import {createPopper} from '@popperjs/core'
2
+ import { Controller } from '@hotwired/stimulus'
3
+ import { createPopper } from '@popperjs/core'
4
4
 
5
5
  export default class extends Controller {
6
6
  static get targets () {
@@ -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";