formstrap 0.4.5 → 0.4.7
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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/formstrap/controllers/media_modal_controller.js +26 -5
- data/app/assets/javascripts/formstrap/controllers/select_controller.js +147 -9
- data/app/assets/javascripts/formstrap.js +148 -12
- data/app/models/formstrap/association_view.rb +24 -2
- data/app/models/formstrap/media_view.rb +1 -1
- data/app/models/formstrap/redactor_view.rb +5 -5
- data/app/models/formstrap/select_view.rb +22 -4
- data/app/models/formstrap/wysiwyg_view.rb +1 -1
- data/app/models/view_model.rb +3 -3
- data/app/views/formstrap/_association.html.erb +32 -10
- data/app/views/formstrap/_select.html.erb +19 -1
- data/app/views/formstrap/media/_modal.html.erb +4 -1
- data/app/views/formstrap/shared/_nested_preview.html.erb +29 -9
- data/lib/formstrap/form_builder.rb +4 -4
- data/lib/formstrap/form_helper.rb +4 -4
- data/lib/formstrap/version.rb +1 -1
- data/package.json +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7b83b9b5c0b00d0d5b3d84a26ca44c16335321c28c6c3e183a84d51fd3c2c45b
|
4
|
+
data.tar.gz: 11d2ec48abdc5ffaaf223d8c3401f3e47a3e6e8d4de813590b8f2543a902ef4e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5cdfdd6829349c2e8e3821bfc3d7ab80b7414f125126d20e9158475e337a1bc7e974a83a0209f6f56e4e45cf115599498f25976a23b616cd9006e54be16a9042
|
7
|
+
data.tar.gz: 22c288a45528576ff1c91c76775003fb0ebeead2bb051c492e3923390e876b49fab4b936924e29b46b5484efd1fa43cfd15f3194fab63bf4e97ea59242d585c0
|
@@ -12,7 +12,10 @@ export default class extends Controller {
|
|
12
12
|
|
13
13
|
connect () {
|
14
14
|
this.validate()
|
15
|
-
|
15
|
+
|
16
|
+
if (this.maxSelectedItems() !== 1) {
|
17
|
+
this.updateCount()
|
18
|
+
}
|
16
19
|
}
|
17
20
|
|
18
21
|
// Actions
|
@@ -27,11 +30,29 @@ export default class extends Controller {
|
|
27
30
|
}
|
28
31
|
|
29
32
|
inputChange (event) {
|
30
|
-
this.
|
31
|
-
|
33
|
+
if (this.maxSelectedItems() === 1) {
|
34
|
+
this.selectOneItem(event.target)
|
35
|
+
} else {
|
36
|
+
this.selectMultipleItems(event.target)
|
37
|
+
}
|
32
38
|
}
|
33
39
|
|
34
40
|
// Methods
|
41
|
+
selectOneItem (element) {
|
42
|
+
this.idsValue = []
|
43
|
+
|
44
|
+
for (const checkbox of this.idCheckboxTargets.filter(e => e.value !== element.value)) {
|
45
|
+
checkbox.checked = false
|
46
|
+
}
|
47
|
+
|
48
|
+
this.handleIdsUpdate(element)
|
49
|
+
}
|
50
|
+
|
51
|
+
selectMultipleItems (element) {
|
52
|
+
this.handleIdsUpdate(element)
|
53
|
+
this.updateCount()
|
54
|
+
}
|
55
|
+
|
35
56
|
hidePlaceholder () {
|
36
57
|
this.placeholderTarget.classList.add('d-none')
|
37
58
|
}
|
@@ -54,7 +75,7 @@ export default class extends Controller {
|
|
54
75
|
})
|
55
76
|
}
|
56
77
|
|
57
|
-
this.
|
78
|
+
this.handleSearchIdsUpdate()
|
58
79
|
}
|
59
80
|
|
60
81
|
itemTargetConnected (element) {
|
@@ -155,7 +176,7 @@ export default class extends Controller {
|
|
155
176
|
this.countTarget.innerHTML = this.selectedItemsCount()
|
156
177
|
}
|
157
178
|
|
158
|
-
|
179
|
+
handleSearchIdsUpdate () {
|
159
180
|
this.deleteSearchIdInputs()
|
160
181
|
this.createSearchIdInputs()
|
161
182
|
}
|
@@ -4,40 +4,159 @@ import I18n from '../config/i18n'
|
|
4
4
|
|
5
5
|
export default class extends Controller {
|
6
6
|
static values = {
|
7
|
-
|
7
|
+
remoteUrl: String,
|
8
|
+
remoteValue: String,
|
9
|
+
remoteLabel: String,
|
10
|
+
remoteQueryParam: String
|
11
|
+
}
|
12
|
+
|
13
|
+
initialize () {
|
14
|
+
this.tomSelect = undefined
|
15
|
+
this.perPage = 24
|
8
16
|
}
|
9
17
|
|
10
18
|
connect () {
|
11
|
-
if (this.
|
12
|
-
this.initTomSelect()
|
19
|
+
if (this.isMultiple() || this.isTomSelect() || this.isRemote()) {
|
20
|
+
this.tomSelect = this.initTomSelect()
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
disconnect () {
|
25
|
+
if (this.element.tomselect) {
|
26
|
+
this.element.tomselect.destroy()
|
13
27
|
}
|
14
28
|
}
|
15
29
|
|
16
30
|
defaultOptions () {
|
17
31
|
return {
|
18
|
-
plugins:
|
32
|
+
plugins: {
|
33
|
+
caret_position: {},
|
34
|
+
drag_drop: {},
|
35
|
+
input_autogrow: {}
|
36
|
+
},
|
19
37
|
persist: false,
|
20
38
|
create: true,
|
21
39
|
render: this.renderOptions()[I18n.locale]
|
22
40
|
}
|
23
41
|
}
|
24
42
|
|
43
|
+
isMultiple () {
|
44
|
+
return this.element.hasAttribute('multiple')
|
45
|
+
}
|
46
|
+
|
47
|
+
isTomSelect () {
|
48
|
+
return this.element.dataset.tomSelect === 'true'
|
49
|
+
}
|
50
|
+
|
51
|
+
isRemote () {
|
52
|
+
return this.remoteUrlValue
|
53
|
+
}
|
54
|
+
|
55
|
+
setQueryParam (url, key, value) {
|
56
|
+
const urlObj = new URL(url)
|
57
|
+
const params = urlObj.searchParams
|
58
|
+
|
59
|
+
// Adds if not exists, updates if exists
|
60
|
+
params.set(key, value)
|
61
|
+
|
62
|
+
return urlObj.toString()
|
63
|
+
}
|
64
|
+
|
65
|
+
getQueryParam (url, key) {
|
66
|
+
const urlObj = new URL(url)
|
67
|
+
const params = urlObj.searchParams
|
68
|
+
|
69
|
+
return params.get(key)
|
70
|
+
}
|
71
|
+
|
72
|
+
firstUrl () {
|
73
|
+
return (query) => {
|
74
|
+
let url = `${this.remoteUrlValue}.json`
|
75
|
+
url = this.setQueryParam(url, this.remoteQueryParamValue, query)
|
76
|
+
url = this.setQueryParam(url, 'per_page', this.perPage)
|
77
|
+
url = this.setQueryParam(url, 'page', 1)
|
78
|
+
return url
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
load () {
|
83
|
+
return (query, callback) => {
|
84
|
+
let url = this.tomSelect.getUrl(query)
|
85
|
+
|
86
|
+
fetch(url)
|
87
|
+
.then(response => response.json())
|
88
|
+
.then(json => {
|
89
|
+
if (json.length === this.perPage) {
|
90
|
+
// Update page param for next call
|
91
|
+
const currentPage = parseInt(this.getQueryParam(url, 'page')) || 1
|
92
|
+
url = this.setQueryParam(url, 'page', currentPage + 1)
|
93
|
+
this.tomSelect.setNextUrl(query, url)
|
94
|
+
} else {
|
95
|
+
this.tomSelect.setNextUrl(query, undefined)
|
96
|
+
}
|
97
|
+
|
98
|
+
callback(json)
|
99
|
+
})
|
100
|
+
.catch(() => { callback() })
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
25
104
|
renderOptions () {
|
26
105
|
return {
|
27
106
|
en: {
|
28
107
|
option_create: function (data, escape) {
|
29
|
-
return
|
108
|
+
return `<div class="create">Add <strong>${escape(data.input)}</strong>…</div>`
|
30
109
|
},
|
31
110
|
no_results: function (data, escape) {
|
32
111
|
return '<div class="no-results">No results found</div>'
|
112
|
+
},
|
113
|
+
loading_more: function (data, escape) {
|
114
|
+
return '<div class="loading-more-results">Loading more results ... </div>'
|
115
|
+
},
|
116
|
+
no_more_results: function (data, escape) {
|
117
|
+
return '<div class="no-more-results">No more results</div>'
|
33
118
|
}
|
34
119
|
},
|
35
120
|
nl: {
|
36
121
|
option_create: function (data, escape) {
|
37
|
-
return
|
122
|
+
return `<div class="create">Voeg <strong>${escape(data.input)}</strong> toe …</div>`
|
38
123
|
},
|
39
124
|
no_results: function (data, escape) {
|
40
125
|
return '<div class="no-results">Geen resultaten gevonden</div>'
|
126
|
+
},
|
127
|
+
loading_more: function (data, escape) {
|
128
|
+
return '<div class="loading-more-results">Laad meer resultaten ... </div>'
|
129
|
+
},
|
130
|
+
no_more_results: function (data, escape) {
|
131
|
+
return '<div class="no-more-results">Geen resultaten meer</div>'
|
132
|
+
}
|
133
|
+
},
|
134
|
+
fr: {
|
135
|
+
option_create: function (data, escape) {
|
136
|
+
return `<div class="create">Ajouter <strong>${escape(data.input)}</strong>…</div>`
|
137
|
+
},
|
138
|
+
no_results: function (data, escape) {
|
139
|
+
return '<div class="no-results">Aucun résultat trouvé</div>'
|
140
|
+
},
|
141
|
+
loading_more: function (data, escape) {
|
142
|
+
return '<div class="loading-more-results">Chargement de plus de résultats ... </div>'
|
143
|
+
},
|
144
|
+
no_more_results: function (data, escape) {
|
145
|
+
return '<div class="no-more-results">Plus de résultats</div>'
|
146
|
+
}
|
147
|
+
},
|
148
|
+
de: {
|
149
|
+
option_create: function (data, escape) {
|
150
|
+
return `<div class="create">Hinzufügen <strong>${escape(data.input)}</strong>…</div>`
|
151
|
+
},
|
152
|
+
no_results: function (data, escape) {
|
153
|
+
return '<div class="no-results">Keine Ergebnisse gefunden</div>'
|
154
|
+
},
|
155
|
+
loading_more: function (data, escape) {
|
156
|
+
return '<div class="loading-more-results">Lade weitere Ergebnisse ... </div>'
|
157
|
+
},
|
158
|
+
no_more_results: function (data, escape) {
|
159
|
+
return '<div class="no-more-results">Keine weiteren Ergebnisse</div>'
|
41
160
|
}
|
42
161
|
}
|
43
162
|
}
|
@@ -51,10 +170,29 @@ export default class extends Controller {
|
|
51
170
|
const defaultOptions = this.defaultOptions()
|
52
171
|
const options = {
|
53
172
|
create: this.hasTags(),
|
54
|
-
|
173
|
+
...(this.isRemote() && {
|
174
|
+
plugins: {
|
175
|
+
caret_position: {},
|
176
|
+
drag_drop: {},
|
177
|
+
input_autogrow: {},
|
178
|
+
virtual_scroll: {}
|
179
|
+
},
|
180
|
+
valueField: this.remoteValueValue,
|
181
|
+
labelField: this.remoteLabelValue,
|
182
|
+
searchField: this.remoteLabelValue,
|
183
|
+
firstUrl: this.firstUrl(),
|
184
|
+
load: this.load(),
|
185
|
+
// Infinite options
|
186
|
+
maxOptions: null,
|
187
|
+
// Fetch first items when focused
|
188
|
+
onFocus: () => {
|
189
|
+
this.tomSelect.clearOptions()
|
190
|
+
this.tomSelect.setNextUrl('', this.firstUrl()(''))
|
191
|
+
this.tomSelect.load('')
|
192
|
+
}
|
193
|
+
})
|
55
194
|
}
|
56
195
|
|
57
|
-
|
58
|
-
new TomSelect(this.element, { ...defaultOptions, ...options })
|
196
|
+
return new TomSelect(this.element, { ...defaultOptions, ...options })
|
59
197
|
}
|
60
198
|
}
|
@@ -11289,7 +11289,9 @@ var media_modal_controller_default = class extends Controller {
|
|
11289
11289
|
}
|
11290
11290
|
connect() {
|
11291
11291
|
this.validate();
|
11292
|
-
this.
|
11292
|
+
if (this.maxSelectedItems() !== 1) {
|
11293
|
+
this.updateCount();
|
11294
|
+
}
|
11293
11295
|
}
|
11294
11296
|
select() {
|
11295
11297
|
this.dispatchSelectionEvent();
|
@@ -11300,7 +11302,21 @@ var media_modal_controller_default = class extends Controller {
|
|
11300
11302
|
this.triggerFormSubmission();
|
11301
11303
|
}
|
11302
11304
|
inputChange(event) {
|
11303
|
-
this.
|
11305
|
+
if (this.maxSelectedItems() === 1) {
|
11306
|
+
this.selectOneItem(event.target);
|
11307
|
+
} else {
|
11308
|
+
this.selectMultipleItems(event.target);
|
11309
|
+
}
|
11310
|
+
}
|
11311
|
+
selectOneItem(element) {
|
11312
|
+
this.idsValue = [];
|
11313
|
+
for (const checkbox of this.idCheckboxTargets.filter((e) => e.value !== element.value)) {
|
11314
|
+
checkbox.checked = false;
|
11315
|
+
}
|
11316
|
+
this.handleIdsUpdate(element);
|
11317
|
+
}
|
11318
|
+
selectMultipleItems(element) {
|
11319
|
+
this.handleIdsUpdate(element);
|
11304
11320
|
this.updateCount();
|
11305
11321
|
}
|
11306
11322
|
hidePlaceholder() {
|
@@ -11321,7 +11337,7 @@ var media_modal_controller_default = class extends Controller {
|
|
11321
11337
|
return element.value !== value;
|
11322
11338
|
});
|
11323
11339
|
}
|
11324
|
-
this.
|
11340
|
+
this.handleSearchIdsUpdate();
|
11325
11341
|
}
|
11326
11342
|
itemTargetConnected(element) {
|
11327
11343
|
this.updateItem(element.querySelector("input"));
|
@@ -11405,7 +11421,7 @@ var media_modal_controller_default = class extends Controller {
|
|
11405
11421
|
updateCount() {
|
11406
11422
|
this.countTarget.innerHTML = this.selectedItemsCount();
|
11407
11423
|
}
|
11408
|
-
|
11424
|
+
handleSearchIdsUpdate() {
|
11409
11425
|
this.deleteSearchIdInputs();
|
11410
11426
|
this.createSearchIdInputs();
|
11411
11427
|
}
|
@@ -13397,35 +13413,134 @@ var repeater_controller_default = class extends Controller {
|
|
13397
13413
|
// app/assets/javascripts/formstrap/controllers/select_controller.js
|
13398
13414
|
var import_tom_select = __toESM(require_tom_select_complete());
|
13399
13415
|
var select_controller_default = class extends Controller {
|
13416
|
+
initialize() {
|
13417
|
+
this.tomSelect = void 0;
|
13418
|
+
this.perPage = 24;
|
13419
|
+
}
|
13400
13420
|
connect() {
|
13401
|
-
if (this.
|
13402
|
-
this.initTomSelect();
|
13421
|
+
if (this.isMultiple() || this.isTomSelect() || this.isRemote()) {
|
13422
|
+
this.tomSelect = this.initTomSelect();
|
13423
|
+
}
|
13424
|
+
}
|
13425
|
+
disconnect() {
|
13426
|
+
if (this.element.tomselect) {
|
13427
|
+
this.element.tomselect.destroy();
|
13403
13428
|
}
|
13404
13429
|
}
|
13405
13430
|
defaultOptions() {
|
13406
13431
|
return {
|
13407
|
-
plugins:
|
13432
|
+
plugins: {
|
13433
|
+
caret_position: {},
|
13434
|
+
drag_drop: {},
|
13435
|
+
input_autogrow: {}
|
13436
|
+
},
|
13408
13437
|
persist: false,
|
13409
13438
|
create: true,
|
13410
13439
|
render: this.renderOptions()[i18n_default.locale]
|
13411
13440
|
};
|
13412
13441
|
}
|
13442
|
+
isMultiple() {
|
13443
|
+
return this.element.hasAttribute("multiple");
|
13444
|
+
}
|
13445
|
+
isTomSelect() {
|
13446
|
+
return this.element.dataset.tomSelect === "true";
|
13447
|
+
}
|
13448
|
+
isRemote() {
|
13449
|
+
return this.remoteUrlValue;
|
13450
|
+
}
|
13451
|
+
setQueryParam(url, key, value) {
|
13452
|
+
const urlObj = new URL(url);
|
13453
|
+
const params = urlObj.searchParams;
|
13454
|
+
params.set(key, value);
|
13455
|
+
return urlObj.toString();
|
13456
|
+
}
|
13457
|
+
getQueryParam(url, key) {
|
13458
|
+
const urlObj = new URL(url);
|
13459
|
+
const params = urlObj.searchParams;
|
13460
|
+
return params.get(key);
|
13461
|
+
}
|
13462
|
+
firstUrl() {
|
13463
|
+
return (query) => {
|
13464
|
+
let url = `${this.remoteUrlValue}.json`;
|
13465
|
+
url = this.setQueryParam(url, this.remoteQueryParamValue, query);
|
13466
|
+
url = this.setQueryParam(url, "per_page", this.perPage);
|
13467
|
+
url = this.setQueryParam(url, "page", 1);
|
13468
|
+
return url;
|
13469
|
+
};
|
13470
|
+
}
|
13471
|
+
load() {
|
13472
|
+
return (query, callback) => {
|
13473
|
+
let url = this.tomSelect.getUrl(query);
|
13474
|
+
fetch(url).then((response) => response.json()).then((json) => {
|
13475
|
+
if (json.length === this.perPage) {
|
13476
|
+
const currentPage = parseInt(this.getQueryParam(url, "page")) || 1;
|
13477
|
+
url = this.setQueryParam(url, "page", currentPage + 1);
|
13478
|
+
this.tomSelect.setNextUrl(query, url);
|
13479
|
+
} else {
|
13480
|
+
this.tomSelect.setNextUrl(query, void 0);
|
13481
|
+
}
|
13482
|
+
callback(json);
|
13483
|
+
}).catch(() => {
|
13484
|
+
callback();
|
13485
|
+
});
|
13486
|
+
};
|
13487
|
+
}
|
13413
13488
|
renderOptions() {
|
13414
13489
|
return {
|
13415
13490
|
en: {
|
13416
13491
|
option_create: function(data, escape) {
|
13417
|
-
return
|
13492
|
+
return `<div class="create">Add <strong>${escape(data.input)}</strong>…</div>`;
|
13418
13493
|
},
|
13419
13494
|
no_results: function(data, escape) {
|
13420
13495
|
return '<div class="no-results">No results found</div>';
|
13496
|
+
},
|
13497
|
+
loading_more: function(data, escape) {
|
13498
|
+
return '<div class="loading-more-results">Loading more results ... </div>';
|
13499
|
+
},
|
13500
|
+
no_more_results: function(data, escape) {
|
13501
|
+
return '<div class="no-more-results">No more results</div>';
|
13421
13502
|
}
|
13422
13503
|
},
|
13423
13504
|
nl: {
|
13424
13505
|
option_create: function(data, escape) {
|
13425
|
-
return
|
13506
|
+
return `<div class="create">Voeg <strong>${escape(data.input)}</strong> toe …</div>`;
|
13426
13507
|
},
|
13427
13508
|
no_results: function(data, escape) {
|
13428
13509
|
return '<div class="no-results">Geen resultaten gevonden</div>';
|
13510
|
+
},
|
13511
|
+
loading_more: function(data, escape) {
|
13512
|
+
return '<div class="loading-more-results">Laad meer resultaten ... </div>';
|
13513
|
+
},
|
13514
|
+
no_more_results: function(data, escape) {
|
13515
|
+
return '<div class="no-more-results">Geen resultaten meer</div>';
|
13516
|
+
}
|
13517
|
+
},
|
13518
|
+
fr: {
|
13519
|
+
option_create: function(data, escape) {
|
13520
|
+
return `<div class="create">Ajouter <strong>${escape(data.input)}</strong>…</div>`;
|
13521
|
+
},
|
13522
|
+
no_results: function(data, escape) {
|
13523
|
+
return '<div class="no-results">Aucun r\xE9sultat trouv\xE9</div>';
|
13524
|
+
},
|
13525
|
+
loading_more: function(data, escape) {
|
13526
|
+
return '<div class="loading-more-results">Chargement de plus de r\xE9sultats ... </div>';
|
13527
|
+
},
|
13528
|
+
no_more_results: function(data, escape) {
|
13529
|
+
return '<div class="no-more-results">Plus de r\xE9sultats</div>';
|
13530
|
+
}
|
13531
|
+
},
|
13532
|
+
de: {
|
13533
|
+
option_create: function(data, escape) {
|
13534
|
+
return `<div class="create">Hinzuf\xFCgen <strong>${escape(data.input)}</strong>…</div>`;
|
13535
|
+
},
|
13536
|
+
no_results: function(data, escape) {
|
13537
|
+
return '<div class="no-results">Keine Ergebnisse gefunden</div>';
|
13538
|
+
},
|
13539
|
+
loading_more: function(data, escape) {
|
13540
|
+
return '<div class="loading-more-results">Lade weitere Ergebnisse ... </div>';
|
13541
|
+
},
|
13542
|
+
no_more_results: function(data, escape) {
|
13543
|
+
return '<div class="no-more-results">Keine weiteren Ergebnisse</div>';
|
13429
13544
|
}
|
13430
13545
|
}
|
13431
13546
|
};
|
@@ -13437,13 +13552,34 @@ var select_controller_default = class extends Controller {
|
|
13437
13552
|
const defaultOptions = this.defaultOptions();
|
13438
13553
|
const options = {
|
13439
13554
|
create: this.hasTags(),
|
13440
|
-
|
13555
|
+
...this.isRemote() && {
|
13556
|
+
plugins: {
|
13557
|
+
caret_position: {},
|
13558
|
+
drag_drop: {},
|
13559
|
+
input_autogrow: {},
|
13560
|
+
virtual_scroll: {}
|
13561
|
+
},
|
13562
|
+
valueField: this.remoteValueValue,
|
13563
|
+
labelField: this.remoteLabelValue,
|
13564
|
+
searchField: this.remoteLabelValue,
|
13565
|
+
firstUrl: this.firstUrl(),
|
13566
|
+
load: this.load(),
|
13567
|
+
maxOptions: null,
|
13568
|
+
onFocus: () => {
|
13569
|
+
this.tomSelect.clearOptions();
|
13570
|
+
this.tomSelect.setNextUrl("", this.firstUrl()(""));
|
13571
|
+
this.tomSelect.load("");
|
13572
|
+
}
|
13573
|
+
}
|
13441
13574
|
};
|
13442
|
-
new import_tom_select.default(this.element, { ...defaultOptions, ...options });
|
13575
|
+
return new import_tom_select.default(this.element, { ...defaultOptions, ...options });
|
13443
13576
|
}
|
13444
13577
|
};
|
13445
13578
|
__publicField(select_controller_default, "values", {
|
13446
|
-
|
13579
|
+
remoteUrl: String,
|
13580
|
+
remoteValue: String,
|
13581
|
+
remoteLabel: String,
|
13582
|
+
remoteQueryParam: String
|
13447
13583
|
});
|
13448
13584
|
|
13449
13585
|
// app/assets/javascripts/formstrap/controllers/textarea_controller.js
|
@@ -36,7 +36,7 @@ module Formstrap
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def attribute_with_id
|
39
|
-
attribute_with_id = collection? ? "#{
|
39
|
+
attribute_with_id = collection? ? "#{singular_name}_ids" : foreign_key
|
40
40
|
|
41
41
|
if attribute_with_id.nil?
|
42
42
|
raise(AssociationDoesNotExistError, "Association attribute that was passed does not exist.")
|
@@ -46,11 +46,28 @@ module Formstrap
|
|
46
46
|
end
|
47
47
|
|
48
48
|
def collection
|
49
|
-
|
49
|
+
if remote
|
50
|
+
collection_for_values
|
51
|
+
else
|
52
|
+
association_class.all.map { |item| [item.to_s, item.id] }
|
53
|
+
end
|
50
54
|
end
|
51
55
|
|
52
56
|
private
|
53
57
|
|
58
|
+
def collection_for_values
|
59
|
+
if collection?
|
60
|
+
form.object.send(attribute).map { |item| option_for_item[item] }
|
61
|
+
else
|
62
|
+
[option_for_item(form.object.send(attribute))]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def option_for_item(item)
|
67
|
+
return unless item
|
68
|
+
[item.send(remote[:label]), item.send(remote[:value])]
|
69
|
+
end
|
70
|
+
|
54
71
|
def association_foreign_key
|
55
72
|
reflection.association_foreign_key
|
56
73
|
end
|
@@ -63,6 +80,10 @@ module Formstrap
|
|
63
80
|
form.object.class.reflect_on_association(attribute)
|
64
81
|
end
|
65
82
|
|
83
|
+
def singular_name
|
84
|
+
reflection.name.to_s.singularize
|
85
|
+
end
|
86
|
+
|
66
87
|
def association_class
|
67
88
|
reflection.klass
|
68
89
|
end
|
@@ -85,6 +106,7 @@ module Formstrap
|
|
85
106
|
tags: tags,
|
86
107
|
controller: "select"
|
87
108
|
},
|
109
|
+
remote: remote,
|
88
110
|
multiple: tags,
|
89
111
|
placeholder: placeholder
|
90
112
|
}
|
@@ -24,7 +24,7 @@ module Formstrap
|
|
24
24
|
lang: I18n.locale,
|
25
25
|
# button to control a block/line in the editor
|
26
26
|
control: false,
|
27
|
-
minHeight:
|
27
|
+
minHeight: "57px",
|
28
28
|
theme: "light",
|
29
29
|
# Popup when highlighting text
|
30
30
|
context: false,
|
@@ -34,11 +34,11 @@ module Formstrap
|
|
34
34
|
# Options in block/line popup
|
35
35
|
control: [],
|
36
36
|
# Options in format popup
|
37
|
-
format: %w[text h1 h2 h3 h4]
|
37
|
+
format: %w[text h1 h2 h3 h4]
|
38
38
|
},
|
39
39
|
block: {
|
40
40
|
# Outline block/line in the editor
|
41
|
-
outline: false
|
41
|
+
outline: false
|
42
42
|
},
|
43
43
|
buttons: {
|
44
44
|
# Options when highlighting text
|
@@ -46,9 +46,9 @@ module Formstrap
|
|
46
46
|
# Options in toolbar on the right
|
47
47
|
extrabar: %w[],
|
48
48
|
# Options in toolbar on the left
|
49
|
-
toolbar: %w[format bold italic deleted list table link html]
|
49
|
+
toolbar: %w[format bold italic deleted list table link html]
|
50
50
|
},
|
51
|
-
plugins: %w[emoji linkstyles]
|
51
|
+
plugins: %w[emoji linkstyles]
|
52
52
|
}.delete_if { |k, v| v.nil? }
|
53
53
|
end
|
54
54
|
end
|
@@ -9,7 +9,7 @@ module Formstrap
|
|
9
9
|
include Formstrap::Wrappable
|
10
10
|
|
11
11
|
def input_options
|
12
|
-
keys = attributes - %i[append attribute collection float form input_group include_blank label prepend validate selected tags wrapper]
|
12
|
+
keys = attributes - %i[append attribute collection float form input_group include_blank label prepend validate selected tags wrapper remote]
|
13
13
|
options = to_h.slice(*keys)
|
14
14
|
default_input_options.deep_merge(options)
|
15
15
|
end
|
@@ -35,9 +35,27 @@ module Formstrap
|
|
35
35
|
private
|
36
36
|
|
37
37
|
def default_options
|
38
|
-
selected = attribute.nil? ? nil : form.object&.send(attribute)
|
39
38
|
{
|
40
|
-
selected:
|
39
|
+
selected: value
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def value
|
44
|
+
attribute.nil? ? nil : form.object&.send(attribute)
|
45
|
+
end
|
46
|
+
|
47
|
+
def is_remote?
|
48
|
+
return false unless remote
|
49
|
+
remote.has_key?(:url)
|
50
|
+
end
|
51
|
+
|
52
|
+
def remote_options
|
53
|
+
return nil unless is_remote?
|
54
|
+
{
|
55
|
+
select_remote_url_value: remote[:url],
|
56
|
+
select_remote_value_value: remote&.dig(:value) || "name",
|
57
|
+
select_remote_label_value: remote&.dig(:label) || "id",
|
58
|
+
select_remote_query_param_value: remote&.dig(:query_param) || "search"
|
41
59
|
}
|
42
60
|
end
|
43
61
|
|
@@ -48,7 +66,7 @@ module Formstrap
|
|
48
66
|
data: {
|
49
67
|
tags: tags,
|
50
68
|
controller: "select",
|
51
|
-
|
69
|
+
**remote_options
|
52
70
|
},
|
53
71
|
multiple: tags,
|
54
72
|
placeholder: placeholder
|
data/app/models/view_model.rb
CHANGED
@@ -24,7 +24,7 @@ class ViewModel
|
|
24
24
|
|
25
25
|
def initialize(hash = {})
|
26
26
|
hash.each do |key, value|
|
27
|
-
instance_variable_set("@#{key}", value)
|
27
|
+
instance_variable_set(:"@#{key}", value)
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
@@ -45,11 +45,11 @@ class ViewModel
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def value_for(attribute)
|
48
|
-
reserved_methods.include?(attribute) ? instance_variable_get("@#{attribute}") : send(attribute)
|
48
|
+
reserved_methods.include?(attribute) ? instance_variable_get(:"@#{attribute}") : send(attribute)
|
49
49
|
end
|
50
50
|
|
51
51
|
def method_missing(m, *args, &block)
|
52
|
-
instance_variable_get("@#{m}")
|
52
|
+
instance_variable_get(:"@#{m}")
|
53
53
|
end
|
54
54
|
|
55
55
|
def respond_to_missing?
|
@@ -6,7 +6,21 @@
|
|
6
6
|
# * +form+ - Form object
|
7
7
|
#
|
8
8
|
# ==== Optional parameters
|
9
|
+
# * +append+ - Display as input group with text on the right-hand side
|
9
10
|
# * +collection+ - Values to create option tags for
|
11
|
+
# * +float+ - Use floating labels. Defaults to false
|
12
|
+
# * +hint+ - Informative text to assist with data input. HTML markup is allowed.
|
13
|
+
# * +label+ - Text to display inside label tag. Defaults to the attribute name. Set to false if you don"t want to show a label.
|
14
|
+
# * +plaintext+ - Render input as plain text.
|
15
|
+
# * +prepend+ - Display as input group with text on the left-hand side
|
16
|
+
# * +remote+ - Hash with all options for remote data fetching
|
17
|
+
# * +wrapper+ - Hash with all options for the surrounding html tag
|
18
|
+
#
|
19
|
+
# ==== Remote options
|
20
|
+
# * +url+ -- JSON endpoint to fetch data from
|
21
|
+
# * +value+ -- JSON attribute to use as the value for the option tag
|
22
|
+
# * +label+ -- JSON attribute to use as the label for the option tag
|
23
|
+
# * +query_param+ -- The query parameter used for searching the json endpoint, default: "search"
|
10
24
|
#
|
11
25
|
#
|
12
26
|
# ==== Examples
|
@@ -14,17 +28,25 @@
|
|
14
28
|
# <%= form_with do |form| %#>
|
15
29
|
# <%= render "formstrap/association", form: form, attribute: :product %#>
|
16
30
|
# <% end %#>
|
31
|
+
#
|
32
|
+
# Remote data
|
33
|
+
# <%= form_with do |form| %#>
|
34
|
+
# <%= render "formstrap/association", form: form, attribute: :product, remote: {url: admin_products_path, value: "id", label: "name"} %#>
|
35
|
+
# <% end %#>
|
17
36
|
|
18
37
|
association = Formstrap::AssociationView.new(local_assigns)
|
19
|
-
|
20
38
|
%>
|
21
39
|
|
22
|
-
<%= render
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
40
|
+
<%= render(
|
41
|
+
"formstrap/select",
|
42
|
+
form: form,
|
43
|
+
attribute: association.attribute_with_id,
|
44
|
+
collection: association.collection,
|
45
|
+
wrapper: association.wrapper_options,
|
46
|
+
float: association.float,
|
47
|
+
hint: association.hint,
|
48
|
+
plaintext: association.plaintext,
|
49
|
+
prepend: association.prepend,
|
50
|
+
append: association.append,
|
51
|
+
remote: association.remote
|
52
|
+
) %>
|
@@ -16,6 +16,13 @@
|
|
16
16
|
# * +prepend+ - Display as input group with text on the left-hand side
|
17
17
|
# * +tags+ - Allow options to be created dynamically. This will set the multiple attribute to true
|
18
18
|
# * +wrapper+ - Hash with all options for the surrounding html tag
|
19
|
+
# * +remote+ - Hash with all options for remote data fetching
|
20
|
+
#
|
21
|
+
# ==== Remote options
|
22
|
+
# * +url+ -- JSON endpoint to fetch data from
|
23
|
+
# * +value+ -- JSON attribute to use as the value for the option tag
|
24
|
+
# * +label+ -- JSON attribute to use as the label for the option tag
|
25
|
+
# * +query_param+ -- The query parameter used for searching the json endpoint
|
19
26
|
#
|
20
27
|
# ==== References
|
21
28
|
# https://headmin.dev/docs/forms/select
|
@@ -27,6 +34,11 @@
|
|
27
34
|
# <%= form_with do |form| %#>
|
28
35
|
# <%= render "formstrap/select", form: form, attribute: :color, collection: %w[red green blue] %#>
|
29
36
|
# <% end %#>
|
37
|
+
#
|
38
|
+
# Remote data
|
39
|
+
# <%= form_with do |form| %#>
|
40
|
+
# <%= render "formstrap/select", form: form, attribute: :id, collection: [["Page 1", 1]], remote: {url: admin_pages_path, value: "id", label: "title"} %#>
|
41
|
+
# <% end %#>
|
30
42
|
|
31
43
|
select = Formstrap::SelectView.new(local_assigns)
|
32
44
|
%>
|
@@ -34,7 +46,13 @@
|
|
34
46
|
<%= render "formstrap/wrapper", select.wrapper_options do %>
|
35
47
|
<%= render "formstrap/label", select.label_options if select.prepend_label? %>
|
36
48
|
<%= render "formstrap/input_group", select.input_group_options do %>
|
37
|
-
<%= form.select(
|
49
|
+
<%= form.select(
|
50
|
+
select.attribute,
|
51
|
+
formstrap: false,
|
52
|
+
choices: select.collection,
|
53
|
+
options: select.select_options,
|
54
|
+
html_options: select.input_options
|
55
|
+
) %>
|
38
56
|
<% end %>
|
39
57
|
<%= render "formstrap/validation", select.validation_options if select.validate? %>
|
40
58
|
<%= render "formstrap/hint", select.hint_options if select.hint? %>
|
@@ -59,7 +59,10 @@
|
|
59
59
|
<% end %>
|
60
60
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><%= t(".close") %></button>
|
61
61
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" data-action="click->media-modal#select" data-media-modal-target="selectButton">
|
62
|
-
<%= t(".select") %>
|
62
|
+
<%= t(".select") %>
|
63
|
+
<% unless max.present? && max.to_i == 1 %>
|
64
|
+
(<span data-media-modal-target="count">0</span><%= t(".maximum", count: max.to_i) if max.present? %>)
|
65
|
+
<% end %>
|
63
66
|
</button>
|
64
67
|
</div>
|
65
68
|
</div>
|
@@ -1,6 +1,17 @@
|
|
1
|
-
<div
|
1
|
+
<div
|
2
|
+
data-controller="nested-preview"
|
3
|
+
data-nested-preview-url-value="<%= url %>"
|
4
|
+
>
|
2
5
|
<!-- Preview placeholder -->
|
3
|
-
<div
|
6
|
+
<div
|
7
|
+
class="nested-preview-iframe-wrapper position-relative"
|
8
|
+
role="button"
|
9
|
+
data-nested-preview-target="iframeWrapper"
|
10
|
+
data-bs-toggle="offcanvas"
|
11
|
+
data-bs-target="#offcanvas-<%= form.options[:child_index] %>"
|
12
|
+
aria-controls="offcanvasRight"
|
13
|
+
data-turbo-temporary
|
14
|
+
>
|
4
15
|
<iframe src="<%= url %>" class="pe-none" data-nested-preview-target="iframe"></iframe>
|
5
16
|
<div data-nested-preview-target="loader" class="nested-preview-loader">
|
6
17
|
<div class="spinner-grow text-secondary" role="status">
|
@@ -8,27 +19,36 @@
|
|
8
19
|
</div>
|
9
20
|
</div>
|
10
21
|
</div>
|
11
|
-
|
12
22
|
<!-- Preview fields wrapper -->
|
13
|
-
<div
|
23
|
+
<div
|
24
|
+
class="offcanvas offcanvas-end nested-preview-offcanvas"
|
25
|
+
tabindex="-1"
|
26
|
+
id="offcanvas-<%= form.options[:child_index] %>"
|
27
|
+
aria-labelledby="offcanvasRightLabel"
|
28
|
+
data-nested-preview-target="offcanvas"
|
29
|
+
>
|
14
30
|
<div class="offcanvas-header">
|
15
|
-
<h5 class="offcanvas-title" id="offcanvasRightLabel"><%= t(
|
16
|
-
<button
|
31
|
+
<h5 class="offcanvas-title" id="offcanvasRightLabel"><%= t(".title", model: form.object.model_name.human) %></h5>
|
32
|
+
<button
|
33
|
+
type="button"
|
34
|
+
class="btn-close"
|
35
|
+
data-bs-dismiss="offcanvas"
|
36
|
+
aria-label="Close"
|
37
|
+
></button>
|
17
38
|
</div>
|
18
39
|
<div class="offcanvas-body">
|
19
40
|
|
20
41
|
<div class="alert alert-danger d-none" data-nested-preview-target="error">
|
21
42
|
<%= t(".error") %>
|
22
43
|
</div>
|
23
|
-
|
24
44
|
<!-- Row content -->
|
25
45
|
<div data-nested-preview-target="fields">
|
26
46
|
<%= yield %>
|
27
47
|
</div>
|
28
|
-
|
29
48
|
<!-- Preview sync button -->
|
30
49
|
<div class="btn btn-primary" data-action="click->nested-preview#update">
|
31
|
-
<%= bootstrap_icon("arrow-repeat") %>
|
50
|
+
<%= bootstrap_icon("arrow-repeat") %>
|
51
|
+
<%= t(".button") %>
|
32
52
|
</div>
|
33
53
|
</div>
|
34
54
|
</div>
|
@@ -11,7 +11,7 @@ module Formstrap
|
|
11
11
|
render_input(:association, attribute, options)
|
12
12
|
end
|
13
13
|
|
14
|
-
def preview_button(url = nil, options = {}, &
|
14
|
+
def preview_button(url = nil, options = {}, &)
|
15
15
|
default_options = {
|
16
16
|
data: {
|
17
17
|
controller: "preview",
|
@@ -19,7 +19,7 @@ module Formstrap
|
|
19
19
|
}
|
20
20
|
}
|
21
21
|
|
22
|
-
@template.render("formstrap/link", form: self, url: url, options: default_options.deep_merge(options), &
|
22
|
+
@template.render("formstrap/link", form: self, url: url, options: default_options.deep_merge(options), &)
|
23
23
|
end
|
24
24
|
|
25
25
|
def checkbox(attribute, formstrap: true, **options)
|
@@ -114,8 +114,8 @@ module Formstrap
|
|
114
114
|
end
|
115
115
|
end
|
116
116
|
|
117
|
-
def repeater_for(attribute, options = {}, &
|
118
|
-
@template.render("formstrap/repeater", form: self, attribute: attribute, **options, &
|
117
|
+
def repeater_for(attribute, options = {}, &)
|
118
|
+
@template.render("formstrap/repeater", form: self, attribute: attribute, **options, &)
|
119
119
|
end
|
120
120
|
|
121
121
|
def redactor(attribute, formstrap: true, **options)
|
@@ -1,15 +1,15 @@
|
|
1
1
|
module Formstrap
|
2
2
|
module FormHelper
|
3
|
-
def formstrap_form_for(record, options = {}, &
|
3
|
+
def formstrap_form_for(record, options = {}, &)
|
4
4
|
# ToDo: Can we pass info about the view here (e.g. host, protocol ...)
|
5
5
|
options = options.reverse_merge({builder: Formstrap::FormBuilder})
|
6
|
-
form_for(record, options, &
|
6
|
+
form_for(record, options, &)
|
7
7
|
end
|
8
8
|
|
9
|
-
def formstrap_form_with(options = {}, &
|
9
|
+
def formstrap_form_with(options = {}, &)
|
10
10
|
# ToDo: Can we pass info about the view here (e.g. host, protocol ...)
|
11
11
|
options = options.reverse_merge({builder: Formstrap::FormBuilder})
|
12
|
-
form_with(**options, &
|
12
|
+
form_with(**options, &)
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
data/lib/formstrap/version.rb
CHANGED
data/package.json
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: formstrap
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jef Vlamings
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-02-10 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: An extensive Bootstrap form library to power your Ruby On Rails application.
|
14
14
|
email:
|