lato_cms 3.0.13 → 3.1.0
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_media_field_controller.js +143 -0
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_media_picker_controller.js +110 -0
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_media_reorder_controller.js +37 -0
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_reload_on_modal_close_controller.js +37 -0
- data/app/assets/stylesheets/lato_cms/application.scss +131 -158
- data/app/controllers/lato_cms/application_controller.rb +4 -0
- data/app/controllers/lato_cms/media_controller.rb +121 -0
- data/app/controllers/lato_cms/pages_controller.rb +75 -91
- data/app/helpers/lato_cms/media_helper.rb +50 -0
- data/app/jobs/lato_cms/export_sitemap_csv_job.rb +33 -0
- data/app/jobs/lato_cms/generate_alt_text_job.rb +21 -0
- data/app/jobs/lato_cms/generate_video_poster_job.rb +7 -5
- data/app/jobs/lato_cms/translate_component_fields_job.rb +16 -0
- data/app/models/lato_cms/llm_client.rb +31 -0
- data/app/models/lato_cms/media.rb +270 -0
- data/app/models/lato_cms/page.rb +40 -0
- data/app/models/lato_cms/page_field.rb +52 -80
- data/app/models/lato_cms/page_field_media.rb +8 -0
- data/app/views/lato_cms/media/_form_create.html.erb +20 -0
- data/app/views/lato_cms/media/_form_update.html.erb +60 -0
- data/app/views/lato_cms/media/create.html.erb +13 -0
- data/app/views/lato_cms/media/index.html.erb +22 -0
- data/app/views/lato_cms/media/picker_action.html.erb +98 -0
- data/app/views/lato_cms/media/update.html.erb +13 -0
- data/app/views/lato_cms/pages/_component_accordion.html.erb +15 -0
- data/app/views/lato_cms/pages/fields/_file.html.erb +4 -55
- data/app/views/lato_cms/pages/fields/_gallery.html.erb +4 -60
- data/app/views/lato_cms/pages/fields/_image.html.erb +4 -68
- data/app/views/lato_cms/pages/fields/_media_field.html.erb +62 -0
- data/app/views/lato_cms/pages/fields/_media_item.html.erb +27 -0
- data/app/views/lato_cms/pages/fields/_video.html.erb +4 -69
- data/app/views/lato_cms/pages/index.html.erb +17 -0
- data/app/views/lato_cms/pages/show.html.erb +2 -1
- data/config/locales/en.yml +36 -12
- data/config/locales/it.yml +36 -12
- data/config/routes.rb +12 -0
- data/db/migrate/20260728175413_create_lato_cms_media.rb +11 -0
- data/db/migrate/20260728175414_create_lato_cms_page_field_media.rb +12 -0
- data/db/migrate/20260729120000_change_lato_cms_media_alt_text_to_translations.rb +25 -0
- data/lib/lato_cms/config.rb +12 -1
- data/lib/lato_cms/version.rb +1 -1
- metadata +38 -5
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_file_field_controller.js +0 -225
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_gallery_field_controller.js +0 -190
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_image_field_controller.js +0 -169
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_video_field_controller.js +0 -180
|
@@ -1,225 +0,0 @@
|
|
|
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
|
-
this.syncRequired()
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
remove(event) {
|
|
34
|
-
const item = event.currentTarget.closest('[data-lato-cms-file-field-target="item"]')
|
|
35
|
-
if (!item) return
|
|
36
|
-
|
|
37
|
-
if (item.dataset.fileUid) {
|
|
38
|
-
this.newFileMap.delete(item.dataset.fileUid)
|
|
39
|
-
item.remove()
|
|
40
|
-
this.syncFileInput()
|
|
41
|
-
if (!this.multipleValue) this.restoreReplacedItems()
|
|
42
|
-
this.syncRequired()
|
|
43
|
-
return
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
item.classList.add('lato-cms-file-field__item--removing')
|
|
47
|
-
item.querySelector('[data-lato-cms-file-field-target="removedNotice"]')?.classList.remove('d-none')
|
|
48
|
-
this.ensureRemoveInput(item)
|
|
49
|
-
this.syncRequired()
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
undo(event) {
|
|
53
|
-
const item = event.currentTarget.closest('[data-lato-cms-file-field-target="item"]')
|
|
54
|
-
if (!item) return
|
|
55
|
-
|
|
56
|
-
item.classList.remove('lato-cms-file-field__item--removing')
|
|
57
|
-
item.querySelector('[data-lato-cms-file-field-target="removedNotice"]')?.classList.add('d-none')
|
|
58
|
-
item.querySelector('[data-remove-file-input]')?.remove()
|
|
59
|
-
this.syncRequired()
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// The file input only has to be required while nothing survives the next save:
|
|
63
|
-
// a pending new file or a persisted attachment not marked for removal already
|
|
64
|
-
// satisfies the field, so keeping `required` would block every re-save (the
|
|
65
|
-
// form saves via fetch without reload, so the attribute must track state).
|
|
66
|
-
syncRequired() {
|
|
67
|
-
const hasActiveAttachment = this.newFileMap.size > 0 ||
|
|
68
|
-
this.itemTargets.some(item => {
|
|
69
|
-
return item.dataset.attachmentId && !item.classList.contains('lato-cms-file-field__item--removing')
|
|
70
|
-
})
|
|
71
|
-
this.inputTarget.required = this.fieldRequired && !hasActiveAttachment
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
ensureRemoveInput(item) {
|
|
75
|
-
if (item.querySelector('[data-remove-file-input]')) return
|
|
76
|
-
|
|
77
|
-
const input = document.createElement('input')
|
|
78
|
-
input.type = 'hidden'
|
|
79
|
-
input.name = this.inputTarget.name.replace(/\[files\]\[\]$/, '[remove_file_ids][]')
|
|
80
|
-
input.value = item.dataset.attachmentId
|
|
81
|
-
input.setAttribute('data-remove-file-input', '')
|
|
82
|
-
item.appendChild(input)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
syncFileInput() {
|
|
86
|
-
const dt = new DataTransfer()
|
|
87
|
-
this.newFileMap.forEach(file => dt.items.add(file))
|
|
88
|
-
this.inputTarget.files = dt.files
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
prepareSingleReplacement() {
|
|
92
|
-
this.newFileMap.clear()
|
|
93
|
-
this.listTarget.querySelectorAll('[data-file-uid]').forEach(item => item.remove())
|
|
94
|
-
this.itemTargets
|
|
95
|
-
.filter(item => item.dataset.attachmentId && !item.querySelector('[data-remove-file-input]'))
|
|
96
|
-
.forEach(item => {
|
|
97
|
-
item.classList.add('d-none')
|
|
98
|
-
item.dataset.replacedByNewFile = 'true'
|
|
99
|
-
})
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
restoreReplacedItems() {
|
|
103
|
-
if (this.newFileMap.size > 0) return
|
|
104
|
-
|
|
105
|
-
this.itemTargets
|
|
106
|
-
.filter(item => item.dataset.replacedByNewFile === 'true')
|
|
107
|
-
.forEach(item => {
|
|
108
|
-
item.classList.remove('d-none')
|
|
109
|
-
delete item.dataset.replacedByNewFile
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
buildNewItem(file, uid) {
|
|
114
|
-
const item = document.createElement('div')
|
|
115
|
-
item.className = 'lato-cms-file-field__item lato-cms-file-field__item--new'
|
|
116
|
-
item.dataset.latoCmsFileFieldTarget = 'item'
|
|
117
|
-
item.dataset.fileUid = uid
|
|
118
|
-
item.innerHTML = `
|
|
119
|
-
<div class="lato-cms-file-field__info">
|
|
120
|
-
<i class="bi bi-paperclip"></i>
|
|
121
|
-
<span>${this.escapeHtml(file.name)}</span>
|
|
122
|
-
<span class="text-muted small">(${this.humanSize(file.size)})</span>
|
|
123
|
-
<span class="badge bg-success">${this.newBadgeLabel}</span>
|
|
124
|
-
</div>
|
|
125
|
-
<button type="button" class="lato-cms-attachment-field__remove" title="${this.removeLabel}" aria-label="${this.removeLabel}" data-action="lato-cms-file-field#remove">
|
|
126
|
-
<i class="bi bi-trash"></i>
|
|
127
|
-
</button>
|
|
128
|
-
`
|
|
129
|
-
return item
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
afterSave(event) {
|
|
133
|
-
if (!event.detail.success) return
|
|
134
|
-
|
|
135
|
-
this.newFileMap.clear()
|
|
136
|
-
this.inputTarget.value = ''
|
|
137
|
-
|
|
138
|
-
const field = this.findSavedField(event.detail.data?.fields || [])
|
|
139
|
-
if (!field) {
|
|
140
|
-
this.itemTargets
|
|
141
|
-
.filter(item => item.querySelector('[data-remove-file-input]'))
|
|
142
|
-
.forEach(item => item.remove())
|
|
143
|
-
this.syncRequired()
|
|
144
|
-
return
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
this.listTarget.innerHTML = ''
|
|
148
|
-
field.attachments.forEach(attachment => {
|
|
149
|
-
this.listTarget.appendChild(this.buildExistingItem(attachment))
|
|
150
|
-
})
|
|
151
|
-
this.syncRequired()
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
findSavedField(fields) {
|
|
155
|
-
return fields.find(field => {
|
|
156
|
-
return field.persisted_field_id === this.fieldId || field.field_id === this.fieldId
|
|
157
|
-
})
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
buildExistingItem(attachment) {
|
|
161
|
-
const item = document.createElement('div')
|
|
162
|
-
item.className = 'lato-cms-file-field__item'
|
|
163
|
-
item.dataset.latoCmsFileFieldTarget = 'item'
|
|
164
|
-
item.dataset.attachmentId = attachment.id
|
|
165
|
-
item.innerHTML = `
|
|
166
|
-
<div class="lato-cms-file-field__info">
|
|
167
|
-
<i class="bi bi-paperclip"></i>
|
|
168
|
-
<span>${this.escapeHtml(attachment.filename)}</span>
|
|
169
|
-
<span class="text-muted small">(${this.humanSize(attachment.byte_size || 0)})</span>
|
|
170
|
-
</div>
|
|
171
|
-
<button type="button" class="lato-cms-attachment-field__remove" title="${this.removeLabel}" aria-label="${this.removeLabel}" data-action="lato-cms-file-field#remove">
|
|
172
|
-
<i class="bi bi-trash"></i>
|
|
173
|
-
</button>
|
|
174
|
-
<div class="lato-cms-file-field__removed d-none" data-lato-cms-file-field-target="removedNotice">
|
|
175
|
-
<span class="text-danger small">
|
|
176
|
-
<i class="bi bi-trash me-1"></i>${this.removePendingLabel}
|
|
177
|
-
</span>
|
|
178
|
-
<button type="button" class="btn btn-link btn-sm p-0" data-action="lato-cms-file-field#undo">
|
|
179
|
-
${this.undoLabel}
|
|
180
|
-
</button>
|
|
181
|
-
</div>
|
|
182
|
-
`
|
|
183
|
-
return item
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
humanSize(size) {
|
|
187
|
-
if (size < 1024) return `${size} B`
|
|
188
|
-
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
|
189
|
-
return `${(size / 1024 / 1024).toFixed(1)} MB`
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
escapeHtml(value) {
|
|
193
|
-
return value.replace(/[&<>"']/g, char => ({
|
|
194
|
-
'&': '&',
|
|
195
|
-
'<': '<',
|
|
196
|
-
'>': '>',
|
|
197
|
-
'"': '"',
|
|
198
|
-
"'": '''
|
|
199
|
-
}[char]))
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
get newBadgeLabel() {
|
|
203
|
-
return this.element.dataset.newBadgeLabel || 'New'
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
get removeLabel() {
|
|
207
|
-
return this.element.dataset.removeLabel || 'Remove'
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
get removePendingLabel() {
|
|
211
|
-
return this.element.dataset.removePendingLabel || 'File will be removed on save'
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
get undoLabel() {
|
|
215
|
-
return this.element.dataset.undoLabel || 'Undo'
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
get fieldId() {
|
|
219
|
-
return this.element.dataset.fieldId
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
get fieldRequired() {
|
|
223
|
-
return this.element.dataset.fieldRequired === 'true'
|
|
224
|
-
}
|
|
225
|
-
}
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
|
|
3
|
-
export default class extends Controller {
|
|
4
|
-
static targets = ["grid", "fileInput", "emptyMsg"]
|
|
5
|
-
static values = { fieldId: String, inputNamePrefix: String }
|
|
6
|
-
|
|
7
|
-
dragging = null
|
|
8
|
-
newFileMap = new Map()
|
|
9
|
-
nextUid = 0
|
|
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
|
-
|
|
20
|
-
// ─── File selection ─────────────────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
addFiles() {
|
|
23
|
-
Array.from(this.fileInputTarget.files).forEach(file => {
|
|
24
|
-
if (!file.type.startsWith('image/')) return
|
|
25
|
-
const uid = `new_${this.nextUid++}`
|
|
26
|
-
this.newFileMap.set(uid, file)
|
|
27
|
-
|
|
28
|
-
const reader = new FileReader()
|
|
29
|
-
reader.onload = ({ target }) => {
|
|
30
|
-
const item = this.buildItem(target.result, null, uid)
|
|
31
|
-
this.gridTarget.appendChild(item)
|
|
32
|
-
this.updateEmpty()
|
|
33
|
-
this.updateOrderInputs()
|
|
34
|
-
}
|
|
35
|
-
reader.readAsDataURL(file)
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
// Reset input then sync with DataTransfer
|
|
39
|
-
this.fileInputTarget.value = ''
|
|
40
|
-
this.syncFileInput()
|
|
41
|
-
this.syncRequired()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ─── Remove ─────────────────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
remove(event) {
|
|
47
|
-
const item = event.currentTarget.closest('[data-gallery-item]')
|
|
48
|
-
if (!item) return
|
|
49
|
-
|
|
50
|
-
const { attachmentId, fileUid } = item.dataset
|
|
51
|
-
|
|
52
|
-
if (attachmentId) {
|
|
53
|
-
const input = document.createElement('input')
|
|
54
|
-
input.type = 'hidden'
|
|
55
|
-
input.name = `${this.inputNamePrefixValue}[remove_file_ids][]`
|
|
56
|
-
input.value = attachmentId
|
|
57
|
-
this.element.appendChild(input)
|
|
58
|
-
} else if (fileUid) {
|
|
59
|
-
this.newFileMap.delete(fileUid)
|
|
60
|
-
this.syncFileInput()
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
item.remove()
|
|
64
|
-
this.updateEmpty()
|
|
65
|
-
this.updateOrderInputs()
|
|
66
|
-
this.syncRequired()
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ─── Drag & drop ─────────────────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
onDragStart(event) {
|
|
72
|
-
this.dragging = event.currentTarget
|
|
73
|
-
event.dataTransfer.effectAllowed = 'move'
|
|
74
|
-
// Defer class so browser can snapshot the element first
|
|
75
|
-
requestAnimationFrame(() => event.currentTarget.classList.add('lato-cms-gallery-field__item--dragging'))
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
onDragEnd(event) {
|
|
79
|
-
event.currentTarget.classList.remove('lato-cms-gallery-field__item--dragging')
|
|
80
|
-
this.dragging = null
|
|
81
|
-
this.updateOrderInputs()
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
onGridDragOver(event) {
|
|
85
|
-
event.preventDefault()
|
|
86
|
-
if (!this.dragging) return
|
|
87
|
-
|
|
88
|
-
const target = event.target.closest('[data-gallery-item]')
|
|
89
|
-
if (!target || target === this.dragging) return
|
|
90
|
-
|
|
91
|
-
const { left, width } = target.getBoundingClientRect()
|
|
92
|
-
const insertBefore = event.clientX < left + width / 2
|
|
93
|
-
this.gridTarget.insertBefore(this.dragging, insertBefore ? target : target.nextSibling)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
onGridDrop(event) {
|
|
97
|
-
event.preventDefault()
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ─── Order inputs ────────────────────────────────────────────────────────────
|
|
101
|
-
|
|
102
|
-
updateOrderInputs() {
|
|
103
|
-
this.element.querySelectorAll('[data-gallery-order-input]').forEach(el => el.remove())
|
|
104
|
-
|
|
105
|
-
this.gridTarget.querySelectorAll('[data-gallery-item][data-attachment-id]').forEach(item => {
|
|
106
|
-
const id = item.dataset.attachmentId
|
|
107
|
-
if (!id) return
|
|
108
|
-
const input = document.createElement('input')
|
|
109
|
-
input.type = 'hidden'
|
|
110
|
-
input.name = `${this.inputNamePrefixValue}[order][]`
|
|
111
|
-
input.value = id
|
|
112
|
-
input.setAttribute('data-gallery-order-input', '')
|
|
113
|
-
this.element.appendChild(input)
|
|
114
|
-
})
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
118
|
-
|
|
119
|
-
syncFileInput() {
|
|
120
|
-
const dt = new DataTransfer()
|
|
121
|
-
this.newFileMap.forEach(file => dt.items.add(file))
|
|
122
|
-
this.fileInputTarget.files = dt.files
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
updateEmpty() {
|
|
126
|
-
if (!this.hasEmptyMsgTarget) return
|
|
127
|
-
const hasItems = this.gridTarget.querySelector('[data-gallery-item]')
|
|
128
|
-
this.emptyMsgTarget.classList.toggle('d-none', !!hasItems)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Required enforcement rides on native form validation: the file input is
|
|
132
|
-
// `visually-hidden` (not display:none) so the browser can focus it and anchor
|
|
133
|
-
// the validation bubble on the "Add images" button. `required` must be set
|
|
134
|
-
// only while no image survives the next save — pending files kept in the
|
|
135
|
-
// input via DataTransfer or surviving grid items satisfy the field.
|
|
136
|
-
syncRequired() {
|
|
137
|
-
const hasActiveItem = this.newFileMap.size > 0 ||
|
|
138
|
-
!!this.gridTarget.querySelector('[data-gallery-item]')
|
|
139
|
-
this.fileInputTarget.required = this.fieldRequired && !hasActiveItem
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
buildItem(src, attachmentId, fileUid) {
|
|
143
|
-
const div = document.createElement('div')
|
|
144
|
-
div.setAttribute('data-gallery-item', '')
|
|
145
|
-
div.dataset.attachmentId = attachmentId || ''
|
|
146
|
-
div.dataset.fileUid = fileUid || ''
|
|
147
|
-
div.draggable = true
|
|
148
|
-
div.className = 'lato-cms-gallery-field__item'
|
|
149
|
-
div.setAttribute('data-action', 'dragstart->lato-cms-gallery-field#onDragStart dragend->lato-cms-gallery-field#onDragEnd')
|
|
150
|
-
div.innerHTML = `
|
|
151
|
-
<img src="${src}" class="lato-cms-gallery-field__thumb" alt="">
|
|
152
|
-
<button type="button" class="lato-cms-attachment-field__remove lato-cms-gallery-field__remove" title="${this.removeLabel}" aria-label="${this.removeLabel}" data-action="click->lato-cms-gallery-field#remove">
|
|
153
|
-
<i class="bi bi-trash"></i>
|
|
154
|
-
</button>
|
|
155
|
-
`
|
|
156
|
-
return div
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
afterSave(event) {
|
|
160
|
-
if (!event.detail.success) return
|
|
161
|
-
|
|
162
|
-
const field = this.findSavedField(event.detail.data?.fields || [])
|
|
163
|
-
if (!field) return
|
|
164
|
-
|
|
165
|
-
this.newFileMap.clear()
|
|
166
|
-
this.fileInputTarget.value = ''
|
|
167
|
-
this.element.querySelectorAll('[name$="[remove_file_ids][]"]').forEach(input => input.remove())
|
|
168
|
-
this.gridTarget.innerHTML = ''
|
|
169
|
-
field.attachments.forEach(attachment => {
|
|
170
|
-
this.gridTarget.appendChild(this.buildItem(attachment.url, attachment.id, null))
|
|
171
|
-
})
|
|
172
|
-
this.updateEmpty()
|
|
173
|
-
this.updateOrderInputs()
|
|
174
|
-
this.syncRequired()
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
findSavedField(fields) {
|
|
178
|
-
return fields.find(field => {
|
|
179
|
-
return field.persisted_field_id === this.fieldIdValue || field.field_id === this.fieldIdValue
|
|
180
|
-
})
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
get removeLabel() {
|
|
184
|
-
return this.element.dataset.removeLabel || 'Remove'
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
get fieldRequired() {
|
|
188
|
-
return this.element.dataset.fieldRequired === 'true'
|
|
189
|
-
}
|
|
190
|
-
}
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
|
|
3
|
-
export default class extends Controller {
|
|
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
|
-
}
|
|
14
|
-
|
|
15
|
-
preview() {
|
|
16
|
-
const file = this.inputTarget.files[0]
|
|
17
|
-
if (!file || !file.type.startsWith('image/')) return
|
|
18
|
-
|
|
19
|
-
const reader = new FileReader()
|
|
20
|
-
reader.onload = ({ target }) => {
|
|
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)})`
|
|
25
|
-
this.newPreviewContainerTarget.classList.remove('d-none')
|
|
26
|
-
this.undo()
|
|
27
|
-
if (this.hasCurrentContainerTarget) this.currentContainerTarget.classList.add('d-none')
|
|
28
|
-
}
|
|
29
|
-
reader.readAsDataURL(file)
|
|
30
|
-
}
|
|
31
|
-
|
|
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-file-field__item--removing')
|
|
46
|
-
this.removedNoticeTarget.classList.remove('d-none')
|
|
47
|
-
this.ensureRemoveInput()
|
|
48
|
-
this.syncRequired()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
undo() {
|
|
52
|
-
if (!this.hasCurrentContainerTarget) return
|
|
53
|
-
|
|
54
|
-
this.currentContainerTarget.classList.remove('lato-cms-file-field__item--removing')
|
|
55
|
-
this.removedNoticeTarget.classList.add('d-none')
|
|
56
|
-
this.removeInput?.remove()
|
|
57
|
-
this.removeInput = null
|
|
58
|
-
this.syncRequired()
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// The file input only has to be required while no attachment survives the next
|
|
62
|
-
// save: a persisted attachment (not marked for removal) already satisfies the
|
|
63
|
-
// field, so keeping `required` on the emptied input would block every re-save.
|
|
64
|
-
syncRequired() {
|
|
65
|
-
const hasActiveAttachment = this.hasCurrentContainerTarget &&
|
|
66
|
-
!this.currentContainerTarget.classList.contains('lato-cms-file-field__item--removing')
|
|
67
|
-
this.inputTarget.required = this.fieldRequired && !hasActiveAttachment
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
ensureRemoveInput() {
|
|
71
|
-
if (this.removeInput) return
|
|
72
|
-
|
|
73
|
-
const input = document.createElement('input')
|
|
74
|
-
input.type = 'hidden'
|
|
75
|
-
input.name = this.inputTarget.name.replace(/\[files\]\[\]$/, '[remove_file_ids][]')
|
|
76
|
-
input.value = this.currentContainerTarget.dataset.attachmentId
|
|
77
|
-
this.element.appendChild(input)
|
|
78
|
-
this.removeInput = input
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
afterSave(event) {
|
|
82
|
-
if (!event.detail.success) return
|
|
83
|
-
|
|
84
|
-
this.inputTarget.value = ''
|
|
85
|
-
this.removeInput = null
|
|
86
|
-
const field = this.findSavedField(event.detail.data?.fields || [])
|
|
87
|
-
if (!field) return
|
|
88
|
-
|
|
89
|
-
const attachment = field.attachments[0]
|
|
90
|
-
this.element.querySelector('[data-lato-cms-image-field-target="currentContainer"]')?.remove()
|
|
91
|
-
this.newPreviewContainerTarget.classList.add('d-none')
|
|
92
|
-
|
|
93
|
-
if (attachment) {
|
|
94
|
-
this.element.insertBefore(this.buildCurrentContainer(attachment), this.newPreviewContainerTarget)
|
|
95
|
-
}
|
|
96
|
-
this.syncRequired()
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
findSavedField(fields) {
|
|
100
|
-
return fields.find(field => {
|
|
101
|
-
return field.persisted_field_id === this.fieldId || field.field_id === this.fieldId
|
|
102
|
-
})
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
buildCurrentContainer(attachment) {
|
|
106
|
-
const container = document.createElement('div')
|
|
107
|
-
container.className = 'lato-cms-file-field__item lato-cms-image-field__item'
|
|
108
|
-
container.dataset.latoCmsImageFieldTarget = 'currentContainer'
|
|
109
|
-
container.dataset.attachmentId = attachment.id
|
|
110
|
-
container.innerHTML = `
|
|
111
|
-
<div class="lato-cms-image-field__preview">
|
|
112
|
-
<img src="${attachment.url}" class="lato-cms-image-field__thumb" data-lato-cms-image-field-target="currentImage" alt="${this.escapeHtml(attachment.filename)}">
|
|
113
|
-
</div>
|
|
114
|
-
<div class="lato-cms-file-field__info">
|
|
115
|
-
<i class="bi bi-image"></i>
|
|
116
|
-
<span>${this.escapeHtml(attachment.filename)}</span>
|
|
117
|
-
<span class="text-muted small">(${this.humanSize(attachment.byte_size || 0)})</span>
|
|
118
|
-
</div>
|
|
119
|
-
<button type="button" class="lato-cms-attachment-field__remove" title="${this.removeLabel}" aria-label="${this.removeLabel}" data-action="lato-cms-image-field#remove">
|
|
120
|
-
<i class="bi bi-trash"></i>
|
|
121
|
-
</button>
|
|
122
|
-
<div class="lato-cms-file-field__removed d-none" data-lato-cms-image-field-target="removedNotice">
|
|
123
|
-
<span class="text-danger small">
|
|
124
|
-
<i class="bi bi-trash me-1"></i>${this.removePendingLabel}
|
|
125
|
-
</span>
|
|
126
|
-
<button type="button" class="btn btn-link btn-sm p-0" data-action="lato-cms-image-field#undo">
|
|
127
|
-
${this.undoLabel}
|
|
128
|
-
</button>
|
|
129
|
-
</div>
|
|
130
|
-
`
|
|
131
|
-
return container
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
humanSize(size) {
|
|
135
|
-
if (size < 1024) return `${size} B`
|
|
136
|
-
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
|
137
|
-
return `${(size / 1024 / 1024).toFixed(1)} MB`
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
escapeHtml(value) {
|
|
141
|
-
return value.replace(/[&<>"']/g, char => ({
|
|
142
|
-
'&': '&',
|
|
143
|
-
'<': '<',
|
|
144
|
-
'>': '>',
|
|
145
|
-
'"': '"',
|
|
146
|
-
"'": '''
|
|
147
|
-
}[char]))
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
get fieldId() {
|
|
151
|
-
return this.element.dataset.fieldId
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
get fieldRequired() {
|
|
155
|
-
return this.element.dataset.fieldRequired === 'true'
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
get removeLabel() {
|
|
159
|
-
return this.element.dataset.removeLabel || 'Remove'
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
get removePendingLabel() {
|
|
163
|
-
return this.element.dataset.removePendingLabel || 'Image will be removed on save'
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
get undoLabel() {
|
|
167
|
-
return this.element.dataset.undoLabel || 'Undo'
|
|
168
|
-
}
|
|
169
|
-
}
|