lato_cms 3.0.0 → 3.0.2
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/lato_cms/controllers/lato_cms_component_form_controller.js +4 -0
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_file_field_controller.js +203 -0
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_gallery_field_controller.js +36 -4
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_image_field_controller.js +135 -6
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_repeater_controller.js +33 -0
- data/app/assets/stylesheets/lato_cms/application.scss +107 -3
- data/app/controllers/lato_cms/pages_controller.rb +120 -49
- data/app/helpers/lato_cms/pages_helper.rb +15 -2
- data/app/models/lato_cms/page.rb +35 -4
- data/app/models/lato_cms/page_field.rb +27 -3
- data/app/models/lato_cms/template_manager.rb +2 -0
- data/app/views/lato_cms/pages/_component_accordion.html.erb +13 -9
- data/app/views/lato_cms/pages/_repeater.html.erb +22 -0
- data/app/views/lato_cms/pages/_repeater_item.html.erb +28 -0
- data/app/views/lato_cms/pages/fields/_boolean.html.erb +6 -4
- data/app/views/lato_cms/pages/fields/_color.html.erb +5 -3
- data/app/views/lato_cms/pages/fields/_date.html.erb +5 -3
- data/app/views/lato_cms/pages/fields/_datetime.html.erb +5 -3
- data/app/views/lato_cms/pages/fields/_file.html.erb +40 -15
- data/app/views/lato_cms/pages/fields/_gallery.html.erb +3 -2
- data/app/views/lato_cms/pages/fields/_image.html.erb +42 -14
- data/app/views/lato_cms/pages/fields/_json.html.erb +5 -3
- data/app/views/lato_cms/pages/fields/_multiselect.html.erb +5 -3
- data/app/views/lato_cms/pages/fields/_number.html.erb +5 -3
- data/app/views/lato_cms/pages/fields/_select.html.erb +5 -3
- data/app/views/lato_cms/pages/fields/_string.html.erb +5 -3
- data/app/views/lato_cms/pages/fields/_text.html.erb +5 -3
- data/app/views/lato_cms/pages/fields/_textarea.html.erb +5 -3
- data/config/locales/en.yml +6 -0
- data/config/locales/it.yml +6 -0
- data/lib/lato_cms/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 527556e5bf27156bab801e7f0c23c53f9ac9e0565d051da5a0fa1c9500954357
|
|
4
|
+
data.tar.gz: dfcbcca5925d763f06d2043e497e128d84d3fcd200059990f9cb8fc1718699cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37384b79131f97266d82434b290a2bdd3ce4b844d1bf9c63bb6363f4877cfa8379ebe1d1fb4ffe2f6bcbc2a00cc69b8e13124b6e9fa9293025ab98606a4690e4
|
|
7
|
+
data.tar.gz: e9ce262f6bbfc70a07554edba5e18ffe9f1c028250da9f6ac5a5b7962977b2f408222e8fddd2eb949f4d48fb1bf2d319c420b1f76ccf3d0675d6a1e39d80c5bb
|
|
@@ -32,6 +32,10 @@ export default class extends Controller {
|
|
|
32
32
|
|
|
33
33
|
if (response.ok) {
|
|
34
34
|
this.showStatus('Fields saved successfully', 'success')
|
|
35
|
+
form.dispatchEvent(new CustomEvent('lato-cms:fields-save-success', {
|
|
36
|
+
bubbles: true,
|
|
37
|
+
detail: { success: true, data }
|
|
38
|
+
}))
|
|
35
39
|
this.refreshPreview()
|
|
36
40
|
} else {
|
|
37
41
|
const errorMsg = data.errors?.map(e => `${e.field_id}: ${e.errors.join(', ')}`).join('; ') || 'Failed to save fields'
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["input", "item", "list", "removedNotice"]
|
|
5
|
+
static values = { multiple: Boolean }
|
|
6
|
+
|
|
7
|
+
newFileMap = new Map()
|
|
8
|
+
nextUid = 0
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
this.afterSave = this.afterSave.bind(this)
|
|
12
|
+
this.element.closest('form')?.addEventListener('lato-cms:fields-save-success', this.afterSave)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
disconnect() {
|
|
16
|
+
this.element.closest('form')?.removeEventListener('lato-cms:fields-save-success', this.afterSave)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
addFiles() {
|
|
20
|
+
if (!this.multipleValue) this.prepareSingleReplacement()
|
|
21
|
+
|
|
22
|
+
Array.from(this.inputTarget.files).forEach(file => {
|
|
23
|
+
const uid = `new_${this.nextUid++}`
|
|
24
|
+
this.newFileMap.set(uid, file)
|
|
25
|
+
this.listTarget.appendChild(this.buildNewItem(file, uid))
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
this.inputTarget.value = ''
|
|
29
|
+
this.syncFileInput()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
remove(event) {
|
|
33
|
+
const item = event.currentTarget.closest('[data-lato-cms-file-field-target="item"]')
|
|
34
|
+
if (!item) return
|
|
35
|
+
|
|
36
|
+
if (item.dataset.fileUid) {
|
|
37
|
+
this.newFileMap.delete(item.dataset.fileUid)
|
|
38
|
+
item.remove()
|
|
39
|
+
this.syncFileInput()
|
|
40
|
+
if (!this.multipleValue) this.restoreReplacedItems()
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
item.classList.add('lato-cms-file-field__item--removing')
|
|
45
|
+
item.querySelector('[data-lato-cms-file-field-target="removedNotice"]')?.classList.remove('d-none')
|
|
46
|
+
this.ensureRemoveInput(item)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
undo(event) {
|
|
50
|
+
const item = event.currentTarget.closest('[data-lato-cms-file-field-target="item"]')
|
|
51
|
+
if (!item) return
|
|
52
|
+
|
|
53
|
+
item.classList.remove('lato-cms-file-field__item--removing')
|
|
54
|
+
item.querySelector('[data-lato-cms-file-field-target="removedNotice"]')?.classList.add('d-none')
|
|
55
|
+
item.querySelector('[data-remove-file-input]')?.remove()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
ensureRemoveInput(item) {
|
|
59
|
+
if (item.querySelector('[data-remove-file-input]')) return
|
|
60
|
+
|
|
61
|
+
const input = document.createElement('input')
|
|
62
|
+
input.type = 'hidden'
|
|
63
|
+
input.name = this.inputTarget.name.replace(/\[files\]\[\]$/, '[remove_file_ids][]')
|
|
64
|
+
input.value = item.dataset.attachmentId
|
|
65
|
+
input.setAttribute('data-remove-file-input', '')
|
|
66
|
+
item.appendChild(input)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
syncFileInput() {
|
|
70
|
+
const dt = new DataTransfer()
|
|
71
|
+
this.newFileMap.forEach(file => dt.items.add(file))
|
|
72
|
+
this.inputTarget.files = dt.files
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
prepareSingleReplacement() {
|
|
76
|
+
this.newFileMap.clear()
|
|
77
|
+
this.listTarget.querySelectorAll('[data-file-uid]').forEach(item => item.remove())
|
|
78
|
+
this.itemTargets
|
|
79
|
+
.filter(item => item.dataset.attachmentId && !item.querySelector('[data-remove-file-input]'))
|
|
80
|
+
.forEach(item => {
|
|
81
|
+
item.classList.add('d-none')
|
|
82
|
+
item.dataset.replacedByNewFile = 'true'
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
restoreReplacedItems() {
|
|
87
|
+
if (this.newFileMap.size > 0) return
|
|
88
|
+
|
|
89
|
+
this.itemTargets
|
|
90
|
+
.filter(item => item.dataset.replacedByNewFile === 'true')
|
|
91
|
+
.forEach(item => {
|
|
92
|
+
item.classList.remove('d-none')
|
|
93
|
+
delete item.dataset.replacedByNewFile
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
buildNewItem(file, uid) {
|
|
98
|
+
const item = document.createElement('div')
|
|
99
|
+
item.className = 'lato-cms-file-field__item lato-cms-file-field__item--new'
|
|
100
|
+
item.dataset.latoCmsFileFieldTarget = 'item'
|
|
101
|
+
item.dataset.fileUid = uid
|
|
102
|
+
item.innerHTML = `
|
|
103
|
+
<div class="lato-cms-file-field__info">
|
|
104
|
+
<i class="bi bi-paperclip"></i>
|
|
105
|
+
<span>${this.escapeHtml(file.name)}</span>
|
|
106
|
+
<span class="text-muted small">(${this.humanSize(file.size)})</span>
|
|
107
|
+
<span class="badge bg-success">${this.newBadgeLabel}</span>
|
|
108
|
+
</div>
|
|
109
|
+
<button type="button" class="btn btn-sm btn-outline-danger" data-action="lato-cms-file-field#remove">
|
|
110
|
+
<i class="bi bi-trash me-1"></i>${this.removeLabel}
|
|
111
|
+
</button>
|
|
112
|
+
`
|
|
113
|
+
return item
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
afterSave(event) {
|
|
117
|
+
if (!event.detail.success) return
|
|
118
|
+
|
|
119
|
+
this.newFileMap.clear()
|
|
120
|
+
this.inputTarget.value = ''
|
|
121
|
+
|
|
122
|
+
const field = this.findSavedField(event.detail.data?.fields || [])
|
|
123
|
+
if (!field) {
|
|
124
|
+
this.itemTargets
|
|
125
|
+
.filter(item => item.querySelector('[data-remove-file-input]'))
|
|
126
|
+
.forEach(item => item.remove())
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.listTarget.innerHTML = ''
|
|
131
|
+
field.attachments.forEach(attachment => {
|
|
132
|
+
this.listTarget.appendChild(this.buildExistingItem(attachment))
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
findSavedField(fields) {
|
|
137
|
+
return fields.find(field => {
|
|
138
|
+
return field.persisted_field_id === this.fieldId || field.field_id === this.fieldId
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
buildExistingItem(attachment) {
|
|
143
|
+
const item = document.createElement('div')
|
|
144
|
+
item.className = 'lato-cms-file-field__item'
|
|
145
|
+
item.dataset.latoCmsFileFieldTarget = 'item'
|
|
146
|
+
item.dataset.attachmentId = attachment.id
|
|
147
|
+
item.innerHTML = `
|
|
148
|
+
<div class="lato-cms-file-field__info">
|
|
149
|
+
<i class="bi bi-paperclip"></i>
|
|
150
|
+
<span>${this.escapeHtml(attachment.filename)}</span>
|
|
151
|
+
<span class="text-muted small">(${this.humanSize(attachment.byte_size || 0)})</span>
|
|
152
|
+
</div>
|
|
153
|
+
<button type="button" class="btn btn-sm btn-outline-danger" data-action="lato-cms-file-field#remove">
|
|
154
|
+
<i class="bi bi-trash me-1"></i>${this.removeLabel}
|
|
155
|
+
</button>
|
|
156
|
+
<div class="lato-cms-file-field__removed d-none" data-lato-cms-file-field-target="removedNotice">
|
|
157
|
+
<span class="text-danger small">
|
|
158
|
+
<i class="bi bi-trash me-1"></i>${this.removePendingLabel}
|
|
159
|
+
</span>
|
|
160
|
+
<button type="button" class="btn btn-link btn-sm p-0" data-action="lato-cms-file-field#undo">
|
|
161
|
+
${this.undoLabel}
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
`
|
|
165
|
+
return item
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
humanSize(size) {
|
|
169
|
+
if (size < 1024) return `${size} B`
|
|
170
|
+
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
|
171
|
+
return `${(size / 1024 / 1024).toFixed(1)} MB`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
escapeHtml(value) {
|
|
175
|
+
return value.replace(/[&<>"']/g, char => ({
|
|
176
|
+
'&': '&',
|
|
177
|
+
'<': '<',
|
|
178
|
+
'>': '>',
|
|
179
|
+
'"': '"',
|
|
180
|
+
"'": '''
|
|
181
|
+
}[char]))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
get newBadgeLabel() {
|
|
185
|
+
return this.element.dataset.newBadgeLabel || 'New'
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
get removeLabel() {
|
|
189
|
+
return this.element.dataset.removeLabel || 'Remove'
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
get removePendingLabel() {
|
|
193
|
+
return this.element.dataset.removePendingLabel || 'File will be removed on save'
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
get undoLabel() {
|
|
197
|
+
return this.element.dataset.undoLabel || 'Undo'
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
get fieldId() {
|
|
201
|
+
return this.element.dataset.fieldId
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -2,12 +2,21 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
2
2
|
|
|
3
3
|
export default class extends Controller {
|
|
4
4
|
static targets = ["grid", "fileInput", "emptyMsg"]
|
|
5
|
-
static values = { fieldId: String }
|
|
5
|
+
static values = { fieldId: String, inputNamePrefix: String }
|
|
6
6
|
|
|
7
7
|
dragging = null
|
|
8
8
|
newFileMap = new Map()
|
|
9
9
|
nextUid = 0
|
|
10
10
|
|
|
11
|
+
connect() {
|
|
12
|
+
this.afterSave = this.afterSave.bind(this)
|
|
13
|
+
this.element.closest('form')?.addEventListener('lato-cms:fields-save-success', this.afterSave)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
disconnect() {
|
|
17
|
+
this.element.closest('form')?.removeEventListener('lato-cms:fields-save-success', this.afterSave)
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
// ─── File selection ─────────────────────────────────────────────────────────
|
|
12
21
|
|
|
13
22
|
addFiles() {
|
|
@@ -42,7 +51,7 @@ export default class extends Controller {
|
|
|
42
51
|
if (attachmentId) {
|
|
43
52
|
const input = document.createElement('input')
|
|
44
53
|
input.type = 'hidden'
|
|
45
|
-
input.name =
|
|
54
|
+
input.name = `${this.inputNamePrefixValue}[remove_file_ids][]`
|
|
46
55
|
input.value = attachmentId
|
|
47
56
|
this.element.appendChild(input)
|
|
48
57
|
} else if (fileUid) {
|
|
@@ -96,7 +105,7 @@ export default class extends Controller {
|
|
|
96
105
|
if (!id) return
|
|
97
106
|
const input = document.createElement('input')
|
|
98
107
|
input.type = 'hidden'
|
|
99
|
-
input.name =
|
|
108
|
+
input.name = `${this.inputNamePrefixValue}[order][]`
|
|
100
109
|
input.value = id
|
|
101
110
|
input.setAttribute('data-gallery-order-input', '')
|
|
102
111
|
this.element.appendChild(input)
|
|
@@ -124,7 +133,7 @@ export default class extends Controller {
|
|
|
124
133
|
div.dataset.fileUid = fileUid || ''
|
|
125
134
|
div.draggable = true
|
|
126
135
|
div.className = 'lato-cms-gallery-field__item'
|
|
127
|
-
div.setAttribute('data-action', 'dragstart->gallery-field#onDragStart dragend->gallery-field#onDragEnd')
|
|
136
|
+
div.setAttribute('data-action', 'dragstart->lato-cms-gallery-field#onDragStart dragend->lato-cms-gallery-field#onDragEnd')
|
|
128
137
|
div.innerHTML = `
|
|
129
138
|
<img src="${src}" class="lato-cms-gallery-field__thumb" alt="">
|
|
130
139
|
<button type="button" class="lato-cms-gallery-field__remove" data-action="click->lato-cms-gallery-field#remove">
|
|
@@ -133,4 +142,27 @@ export default class extends Controller {
|
|
|
133
142
|
`
|
|
134
143
|
return div
|
|
135
144
|
}
|
|
145
|
+
|
|
146
|
+
afterSave(event) {
|
|
147
|
+
if (!event.detail.success) return
|
|
148
|
+
|
|
149
|
+
const field = this.findSavedField(event.detail.data?.fields || [])
|
|
150
|
+
if (!field) return
|
|
151
|
+
|
|
152
|
+
this.newFileMap.clear()
|
|
153
|
+
this.fileInputTarget.value = ''
|
|
154
|
+
this.element.querySelectorAll('[name$="[remove_file_ids][]"]').forEach(input => input.remove())
|
|
155
|
+
this.gridTarget.innerHTML = ''
|
|
156
|
+
field.attachments.forEach(attachment => {
|
|
157
|
+
this.gridTarget.appendChild(this.buildItem(attachment.url, attachment.id, null))
|
|
158
|
+
})
|
|
159
|
+
this.updateEmpty()
|
|
160
|
+
this.updateOrderInputs()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
findSavedField(fields) {
|
|
164
|
+
return fields.find(field => {
|
|
165
|
+
return field.persisted_field_id === this.fieldIdValue || field.field_id === this.fieldIdValue
|
|
166
|
+
})
|
|
167
|
+
}
|
|
136
168
|
}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
3
|
export default class extends Controller {
|
|
4
|
-
static targets = ["input", "newPreview", "newPreviewContainer", "currentContainer", "currentImage", "
|
|
4
|
+
static targets = ["input", "newPreview", "newPreviewContainer", "newPreviewName", "newPreviewSize", "currentContainer", "currentImage", "removedNotice"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
this.afterSave = this.afterSave.bind(this)
|
|
8
|
+
this.element.closest('form')?.addEventListener('lato-cms:fields-save-success', this.afterSave)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
disconnect() {
|
|
12
|
+
this.element.closest('form')?.removeEventListener('lato-cms:fields-save-success', this.afterSave)
|
|
13
|
+
}
|
|
5
14
|
|
|
6
15
|
preview() {
|
|
7
16
|
const file = this.inputTarget.files[0]
|
|
@@ -10,15 +19,135 @@ export default class extends Controller {
|
|
|
10
19
|
const reader = new FileReader()
|
|
11
20
|
reader.onload = ({ target }) => {
|
|
12
21
|
this.newPreviewTarget.src = target.result
|
|
22
|
+
this.newPreviewTarget.alt = file.name
|
|
23
|
+
this.newPreviewNameTarget.textContent = file.name
|
|
24
|
+
this.newPreviewSizeTarget.textContent = `(${this.humanSize(file.size)})`
|
|
13
25
|
this.newPreviewContainerTarget.classList.remove('d-none')
|
|
26
|
+
this.undo()
|
|
27
|
+
if (this.hasCurrentContainerTarget) this.currentContainerTarget.classList.add('d-none')
|
|
14
28
|
}
|
|
15
29
|
reader.readAsDataURL(file)
|
|
16
30
|
}
|
|
17
31
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.
|
|
22
|
-
this.
|
|
32
|
+
removeNew() {
|
|
33
|
+
this.inputTarget.value = ''
|
|
34
|
+
this.newPreviewTarget.src = ''
|
|
35
|
+
this.newPreviewTarget.alt = ''
|
|
36
|
+
this.newPreviewNameTarget.textContent = ''
|
|
37
|
+
this.newPreviewSizeTarget.textContent = ''
|
|
38
|
+
this.newPreviewContainerTarget.classList.add('d-none')
|
|
39
|
+
if (this.hasCurrentContainerTarget) this.currentContainerTarget.classList.remove('d-none')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
remove() {
|
|
43
|
+
if (!this.hasCurrentContainerTarget) return
|
|
44
|
+
|
|
45
|
+
this.currentContainerTarget.classList.add('lato-cms-image-field--removing')
|
|
46
|
+
this.removedNoticeTarget.classList.remove('d-none')
|
|
47
|
+
this.ensureRemoveInput()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
undo() {
|
|
51
|
+
if (!this.hasCurrentContainerTarget) return
|
|
52
|
+
|
|
53
|
+
this.currentContainerTarget.classList.remove('lato-cms-image-field--removing')
|
|
54
|
+
this.removedNoticeTarget.classList.add('d-none')
|
|
55
|
+
this.removeInput?.remove()
|
|
56
|
+
this.removeInput = null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
ensureRemoveInput() {
|
|
60
|
+
if (this.removeInput) return
|
|
61
|
+
|
|
62
|
+
const input = document.createElement('input')
|
|
63
|
+
input.type = 'hidden'
|
|
64
|
+
input.name = this.inputTarget.name.replace(/\[files\]\[\]$/, '[remove_file_ids][]')
|
|
65
|
+
input.value = this.currentContainerTarget.dataset.attachmentId
|
|
66
|
+
this.element.appendChild(input)
|
|
67
|
+
this.removeInput = input
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
afterSave(event) {
|
|
71
|
+
if (!event.detail.success) return
|
|
72
|
+
|
|
73
|
+
this.inputTarget.value = ''
|
|
74
|
+
this.removeInput = null
|
|
75
|
+
const field = this.findSavedField(event.detail.data?.fields || [])
|
|
76
|
+
if (!field) return
|
|
77
|
+
|
|
78
|
+
const attachment = field.attachments[0]
|
|
79
|
+
this.element.querySelector('[data-lato-cms-image-field-target="currentContainer"]')?.remove()
|
|
80
|
+
this.newPreviewContainerTarget.classList.add('d-none')
|
|
81
|
+
|
|
82
|
+
if (attachment) {
|
|
83
|
+
this.element.insertBefore(this.buildCurrentContainer(attachment), this.newPreviewContainerTarget)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
findSavedField(fields) {
|
|
88
|
+
return fields.find(field => {
|
|
89
|
+
return field.persisted_field_id === this.fieldId || field.field_id === this.fieldId
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
buildCurrentContainer(attachment) {
|
|
94
|
+
const container = document.createElement('div')
|
|
95
|
+
container.className = 'lato-cms-file-field__item lato-cms-image-field__item'
|
|
96
|
+
container.dataset.latoCmsImageFieldTarget = 'currentContainer'
|
|
97
|
+
container.dataset.attachmentId = attachment.id
|
|
98
|
+
container.innerHTML = `
|
|
99
|
+
<div class="lato-cms-image-field__preview">
|
|
100
|
+
<img src="${attachment.url}" class="lato-cms-image-field__thumb" data-lato-cms-image-field-target="currentImage" alt="${this.escapeHtml(attachment.filename)}">
|
|
101
|
+
</div>
|
|
102
|
+
<div class="lato-cms-file-field__info">
|
|
103
|
+
<i class="bi bi-image"></i>
|
|
104
|
+
<span>${this.escapeHtml(attachment.filename)}</span>
|
|
105
|
+
<span class="text-muted small">(${this.humanSize(attachment.byte_size || 0)})</span>
|
|
106
|
+
</div>
|
|
107
|
+
<button type="button" class="btn btn-sm btn-outline-danger" data-action="lato-cms-image-field#remove">
|
|
108
|
+
<i class="bi bi-trash me-1"></i>${this.removeLabel}
|
|
109
|
+
</button>
|
|
110
|
+
<div class="lato-cms-image-field__removed d-none" data-lato-cms-image-field-target="removedNotice">
|
|
111
|
+
<span class="text-danger small">
|
|
112
|
+
<i class="bi bi-trash me-1"></i>${this.removePendingLabel}
|
|
113
|
+
</span>
|
|
114
|
+
<button type="button" class="btn btn-link btn-sm p-0" data-action="lato-cms-image-field#undo">
|
|
115
|
+
${this.undoLabel}
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
`
|
|
119
|
+
return container
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
humanSize(size) {
|
|
123
|
+
if (size < 1024) return `${size} B`
|
|
124
|
+
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
|
125
|
+
return `${(size / 1024 / 1024).toFixed(1)} MB`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
escapeHtml(value) {
|
|
129
|
+
return value.replace(/[&<>"']/g, char => ({
|
|
130
|
+
'&': '&',
|
|
131
|
+
'<': '<',
|
|
132
|
+
'>': '>',
|
|
133
|
+
'"': '"',
|
|
134
|
+
"'": '''
|
|
135
|
+
}[char]))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
get fieldId() {
|
|
139
|
+
return this.element.dataset.fieldId
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get removeLabel() {
|
|
143
|
+
return this.element.dataset.removeLabel || 'Remove'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get removePendingLabel() {
|
|
147
|
+
return this.element.dataset.removePendingLabel || 'Image will be removed on save'
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get undoLabel() {
|
|
151
|
+
return this.element.dataset.undoLabel || 'Undo'
|
|
23
152
|
}
|
|
24
153
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus'
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ['items', 'template', 'item', 'position', 'orderInput']
|
|
5
|
+
|
|
6
|
+
add () {
|
|
7
|
+
const itemId = window.crypto.randomUUID()
|
|
8
|
+
const html = this.templateTarget.innerHTML
|
|
9
|
+
.replaceAll('NEW_RECORD', itemId)
|
|
10
|
+
.replaceAll('NEW_INDEX', this.itemTargets.length.toString())
|
|
11
|
+
|
|
12
|
+
this.itemsTarget.insertAdjacentHTML('beforeend', html)
|
|
13
|
+
this.refresh()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
remove (event) {
|
|
17
|
+
const item = event.currentTarget.closest('[data-lato-cms-repeater-target="item"]')
|
|
18
|
+
if (!item) return
|
|
19
|
+
|
|
20
|
+
item.remove()
|
|
21
|
+
this.refresh()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
refresh () {
|
|
25
|
+
this.itemTargets.forEach((item, index) => {
|
|
26
|
+
const position = item.querySelector('[data-lato-cms-repeater-target="position"]')
|
|
27
|
+
if (position) position.textContent = index + 1
|
|
28
|
+
|
|
29
|
+
const orderInput = item.querySelector('[data-lato-cms-repeater-target="orderInput"]')
|
|
30
|
+
if (orderInput) orderInput.value = item.dataset.repeaterItemId
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -142,13 +142,99 @@ body.lato-cms-advanced-editor--open {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// ── File field ─────────────────────────────────────────────────────────────────
|
|
146
|
+
.lato-cms-file-field__item {
|
|
147
|
+
align-items: center;
|
|
148
|
+
background: #fff;
|
|
149
|
+
border: 1px solid rgba(33, 37, 41, 0.12);
|
|
150
|
+
border-radius: 8px;
|
|
151
|
+
box-shadow: 0 1px 2px rgba(33, 37, 41, 0.04);
|
|
152
|
+
display: flex;
|
|
153
|
+
gap: 0.75rem;
|
|
154
|
+
justify-content: space-between;
|
|
155
|
+
margin-bottom: 0.6rem;
|
|
156
|
+
min-height: 56px;
|
|
157
|
+
padding: 0.65rem 0.75rem;
|
|
158
|
+
transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
|
|
159
|
+
|
|
160
|
+
&:hover {
|
|
161
|
+
border-color: rgba(13, 110, 253, 0.25);
|
|
162
|
+
box-shadow: 0 3px 10px rgba(33, 37, 41, 0.07);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.lato-cms-file-field__info {
|
|
167
|
+
align-items: center;
|
|
168
|
+
display: flex;
|
|
169
|
+
flex-wrap: wrap;
|
|
170
|
+
gap: 0.35rem;
|
|
171
|
+
min-width: 0;
|
|
172
|
+
flex: 1;
|
|
173
|
+
|
|
174
|
+
> span:first-of-type {
|
|
175
|
+
overflow-wrap: anywhere;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
> .bi {
|
|
179
|
+
color: var(--bs-secondary);
|
|
180
|
+
font-size: 1rem;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.lato-cms-file-field__removed {
|
|
185
|
+
align-items: center;
|
|
186
|
+
display: flex;
|
|
187
|
+
gap: 0.75rem;
|
|
188
|
+
justify-content: space-between;
|
|
189
|
+
width: 100%;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.lato-cms-file-field__item--removing {
|
|
193
|
+
background: rgba(220, 53, 69, 0.06);
|
|
194
|
+
border-color: rgba(220, 53, 69, 0.35);
|
|
195
|
+
box-shadow: none;
|
|
196
|
+
|
|
197
|
+
.lato-cms-file-field__info,
|
|
198
|
+
.lato-cms-image-field__preview,
|
|
199
|
+
> .btn {
|
|
200
|
+
display: none;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.lato-cms-file-field__item--new {
|
|
205
|
+
background: rgba(25, 135, 84, 0.04);
|
|
206
|
+
border-color: rgba(25, 135, 84, 0.45);
|
|
207
|
+
|
|
208
|
+
.badge {
|
|
209
|
+
font-weight: 500;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
145
213
|
// ── Image field ────────────────────────────────────────────────────────────────
|
|
214
|
+
.lato-cms-image-field__item {
|
|
215
|
+
align-items: stretch;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.lato-cms-image-field__preview {
|
|
219
|
+
align-items: center;
|
|
220
|
+
align-self: center;
|
|
221
|
+
background: var(--bs-light);
|
|
222
|
+
border: 1px solid rgba(33, 37, 41, 0.08);
|
|
223
|
+
border-radius: 6px;
|
|
224
|
+
display: flex;
|
|
225
|
+
flex: 0 0 auto;
|
|
226
|
+
height: 64px;
|
|
227
|
+
justify-content: center;
|
|
228
|
+
overflow: hidden;
|
|
229
|
+
width: 64px;
|
|
230
|
+
}
|
|
231
|
+
|
|
146
232
|
.lato-cms-image-field__thumb {
|
|
147
|
-
|
|
148
|
-
|
|
233
|
+
width: 100%;
|
|
234
|
+
height: 100%;
|
|
149
235
|
object-fit: cover;
|
|
150
236
|
display: block;
|
|
151
|
-
transition: opacity 0.15s,
|
|
237
|
+
transition: opacity 0.15s, filter 0.15s;
|
|
152
238
|
}
|
|
153
239
|
|
|
154
240
|
.lato-cms-image-field__remove-btn {
|
|
@@ -158,6 +244,7 @@ body.lato-cms-advanced-editor--open {
|
|
|
158
244
|
width: 22px;
|
|
159
245
|
height: 22px;
|
|
160
246
|
border-radius: 50%;
|
|
247
|
+
border: 0;
|
|
161
248
|
background: rgba(220, 53, 69, 0.85);
|
|
162
249
|
color: #fff;
|
|
163
250
|
display: flex;
|
|
@@ -172,6 +259,23 @@ body.lato-cms-advanced-editor--open {
|
|
|
172
259
|
}
|
|
173
260
|
}
|
|
174
261
|
|
|
262
|
+
.lato-cms-image-field__removed {
|
|
263
|
+
align-items: center;
|
|
264
|
+
display: flex;
|
|
265
|
+
gap: 0.75rem;
|
|
266
|
+
justify-content: space-between;
|
|
267
|
+
width: 100%;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.lato-cms-image-field--removing {
|
|
271
|
+
.lato-cms-image-field__preview,
|
|
272
|
+
.lato-cms-file-field__info,
|
|
273
|
+
.lato-cms-image-field__remove-btn,
|
|
274
|
+
> .btn {
|
|
275
|
+
display: none;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
175
279
|
.lato-cms-image-field__new-badge {
|
|
176
280
|
position: absolute;
|
|
177
281
|
bottom: 4px;
|