lato_cms 3.0.13 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +44 -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,180 +0,0 @@
|
|
|
1
|
-
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
|
|
3
|
-
export default class extends Controller {
|
|
4
|
-
static targets = ["input", "newPreview", "newPreviewContainer", "newPreviewName", "newPreviewSize", "currentContainer", "currentVideo", "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
|
-
this.revokeObjectUrl()
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
preview() {
|
|
17
|
-
const file = this.inputTarget.files[0]
|
|
18
|
-
if (!file || !file.type.startsWith('video/')) return
|
|
19
|
-
|
|
20
|
-
// Object URL instead of FileReader: videos can be large and the browser
|
|
21
|
-
// streams from the original file without loading it in memory.
|
|
22
|
-
this.revokeObjectUrl()
|
|
23
|
-
this.objectUrl = URL.createObjectURL(file)
|
|
24
|
-
this.newPreviewTarget.src = this.objectUrl
|
|
25
|
-
this.newPreviewNameTarget.textContent = file.name
|
|
26
|
-
this.newPreviewSizeTarget.textContent = `(${this.humanSize(file.size)})`
|
|
27
|
-
this.newPreviewContainerTarget.classList.remove('d-none')
|
|
28
|
-
this.undo()
|
|
29
|
-
if (this.hasCurrentContainerTarget) this.currentContainerTarget.classList.add('d-none')
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
removeNew() {
|
|
33
|
-
this.inputTarget.value = ''
|
|
34
|
-
this.revokeObjectUrl()
|
|
35
|
-
this.newPreviewTarget.removeAttribute('src')
|
|
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
|
-
this.revokeObjectUrl()
|
|
87
|
-
const field = this.findSavedField(event.detail.data?.fields || [])
|
|
88
|
-
if (!field) return
|
|
89
|
-
|
|
90
|
-
const attachment = field.attachments[0]
|
|
91
|
-
this.element.querySelector('[data-lato-cms-video-field-target="currentContainer"]')?.remove()
|
|
92
|
-
this.newPreviewContainerTarget.classList.add('d-none')
|
|
93
|
-
|
|
94
|
-
if (attachment) {
|
|
95
|
-
this.element.insertBefore(this.buildCurrentContainer(attachment), this.newPreviewContainerTarget)
|
|
96
|
-
}
|
|
97
|
-
this.syncRequired()
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
findSavedField(fields) {
|
|
101
|
-
return fields.find(field => {
|
|
102
|
-
return field.persisted_field_id === this.fieldId || field.field_id === this.fieldId
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
buildCurrentContainer(attachment) {
|
|
107
|
-
const container = document.createElement('div')
|
|
108
|
-
container.className = 'lato-cms-file-field__item lato-cms-video-field__item'
|
|
109
|
-
container.dataset.latoCmsVideoFieldTarget = 'currentContainer'
|
|
110
|
-
container.dataset.attachmentId = attachment.id
|
|
111
|
-
// poster_url is usually still null right after save: the poster is
|
|
112
|
-
// generated by a background job and shows up on the next page load.
|
|
113
|
-
const poster = attachment.poster_url ? ` poster="${attachment.poster_url}"` : ''
|
|
114
|
-
container.innerHTML = `
|
|
115
|
-
<div class="lato-cms-video-field__preview">
|
|
116
|
-
<video controls preload="metadata" class="lato-cms-video-field__player" src="${attachment.url}"${poster} data-lato-cms-video-field-target="currentVideo"></video>
|
|
117
|
-
</div>
|
|
118
|
-
<div class="lato-cms-file-field__info">
|
|
119
|
-
<i class="bi bi-film"></i>
|
|
120
|
-
<span>${this.escapeHtml(attachment.filename)}</span>
|
|
121
|
-
<span class="text-muted small">(${this.humanSize(attachment.byte_size || 0)})</span>
|
|
122
|
-
</div>
|
|
123
|
-
<button type="button" class="lato-cms-attachment-field__remove" title="${this.removeLabel}" aria-label="${this.removeLabel}" data-action="lato-cms-video-field#remove">
|
|
124
|
-
<i class="bi bi-trash"></i>
|
|
125
|
-
</button>
|
|
126
|
-
<div class="lato-cms-file-field__removed d-none" data-lato-cms-video-field-target="removedNotice">
|
|
127
|
-
<span class="text-danger small">
|
|
128
|
-
<i class="bi bi-trash me-1"></i>${this.removePendingLabel}
|
|
129
|
-
</span>
|
|
130
|
-
<button type="button" class="btn btn-link btn-sm p-0" data-action="lato-cms-video-field#undo">
|
|
131
|
-
${this.undoLabel}
|
|
132
|
-
</button>
|
|
133
|
-
</div>
|
|
134
|
-
`
|
|
135
|
-
return container
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
revokeObjectUrl() {
|
|
139
|
-
if (!this.objectUrl) return
|
|
140
|
-
|
|
141
|
-
URL.revokeObjectURL(this.objectUrl)
|
|
142
|
-
this.objectUrl = null
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
humanSize(size) {
|
|
146
|
-
if (size < 1024) return `${size} B`
|
|
147
|
-
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
|
148
|
-
return `${(size / 1024 / 1024).toFixed(1)} MB`
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
escapeHtml(value) {
|
|
152
|
-
return value.replace(/[&<>"']/g, char => ({
|
|
153
|
-
'&': '&',
|
|
154
|
-
'<': '<',
|
|
155
|
-
'>': '>',
|
|
156
|
-
'"': '"',
|
|
157
|
-
"'": '''
|
|
158
|
-
}[char]))
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
get fieldId() {
|
|
162
|
-
return this.element.dataset.fieldId
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
get fieldRequired() {
|
|
166
|
-
return this.element.dataset.fieldRequired === 'true'
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
get removeLabel() {
|
|
170
|
-
return this.element.dataset.removeLabel || 'Remove'
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
get removePendingLabel() {
|
|
174
|
-
return this.element.dataset.removePendingLabel || 'Video will be removed on save'
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
get undoLabel() {
|
|
178
|
-
return this.element.dataset.undoLabel || 'Undo'
|
|
179
|
-
}
|
|
180
|
-
}
|