lato_cms 3.0.9 → 3.0.11
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_repeater_controller.js +29 -1
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_video_field_controller.js +164 -0
- data/app/assets/stylesheets/lato_cms/application.scss +22 -0
- data/app/controllers/lato_cms/pages_controller.rb +32 -0
- data/app/helpers/lato_cms/pages_helper.rb +1 -1
- data/app/jobs/lato_cms/generate_video_poster_job.rb +12 -0
- data/app/models/lato_cms/page_field.rb +44 -0
- data/app/views/lato_cms/pages/_repeater.html.erb +8 -3
- data/app/views/lato_cms/pages/fields/_video.html.erb +68 -0
- data/config/locales/en.yml +5 -0
- data/config/locales/it.yml +5 -0
- data/lib/lato_cms/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 18323c23906137ac104d760d833ca76cd64041f254372a8a7e037bee23a5d5c5
|
|
4
|
+
data.tar.gz: 5324f43d1305e9aea41bcf19b2b41c96e555edd70ace601b788e649f5cb80b7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d1c86cbe7a2a9dc66ffad24bb681f5a9022243c86f39ea4f2e8273c5500efec5f5ea20f610979a2f1bc49a4e85e96f1c5471faf00a5d42011b5df02da18cff5a
|
|
7
|
+
data.tar.gz: eebc417e788f0275c9b6daf1e50777487008358f8b080b75b48cab6f192f1cea94d70f177da537f881ee723b90cf58d365bdcf51f197dc2f9dc0c88c072c9919
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus'
|
|
2
2
|
|
|
3
3
|
export default class extends Controller {
|
|
4
|
-
static targets = ['items', 'template', 'item', 'position', 'orderInput']
|
|
4
|
+
static targets = ['items', 'template', 'item', 'position', 'orderInput', 'addButton']
|
|
5
|
+
static values = { min: { type: Number, default: 0 }, max: { type: Number, default: 0 } }
|
|
6
|
+
|
|
7
|
+
connect () {
|
|
8
|
+
this.refresh()
|
|
9
|
+
}
|
|
5
10
|
|
|
6
11
|
add () {
|
|
12
|
+
// Guard against exceeding max even if the button state is stale.
|
|
13
|
+
if (this.maxValue > 0 && this.itemTargets.length >= this.maxValue) return
|
|
14
|
+
|
|
7
15
|
const itemId = window.crypto.randomUUID()
|
|
8
16
|
const html = this.templateTarget.innerHTML
|
|
9
17
|
.replaceAll('NEW_RECORD', itemId)
|
|
@@ -14,6 +22,9 @@ export default class extends Controller {
|
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
remove (event) {
|
|
25
|
+
// Block removal below the required minimum.
|
|
26
|
+
if (this.minValue > 0 && this.itemTargets.length <= this.minValue) return
|
|
27
|
+
|
|
17
28
|
const item = event.currentTarget.closest('[data-lato-cms-repeater-target="item"]')
|
|
18
29
|
if (!item) return
|
|
19
30
|
|
|
@@ -29,5 +40,22 @@ export default class extends Controller {
|
|
|
29
40
|
const orderInput = item.querySelector('[data-lato-cms-repeater-target="orderInput"]')
|
|
30
41
|
if (orderInput) orderInput.value = item.dataset.repeaterItemId
|
|
31
42
|
})
|
|
43
|
+
|
|
44
|
+
this.updateControls()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Toggle add/remove availability against the min/max constraints.
|
|
48
|
+
updateControls () {
|
|
49
|
+
const count = this.itemTargets.length
|
|
50
|
+
|
|
51
|
+
if (this.hasAddButtonTarget) {
|
|
52
|
+
this.addButtonTarget.disabled = this.maxValue > 0 && count >= this.maxValue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const disableRemove = this.minValue > 0 && count <= this.minValue
|
|
56
|
+
this.itemTargets.forEach((item) => {
|
|
57
|
+
const removeButton = item.querySelector('[data-action~="lato-cms-repeater#remove"]')
|
|
58
|
+
if (removeButton) removeButton.disabled = disableRemove
|
|
59
|
+
})
|
|
32
60
|
}
|
|
33
61
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
}
|
|
49
|
+
|
|
50
|
+
undo() {
|
|
51
|
+
if (!this.hasCurrentContainerTarget) return
|
|
52
|
+
|
|
53
|
+
this.currentContainerTarget.classList.remove('lato-cms-file-field__item--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
|
+
this.revokeObjectUrl()
|
|
76
|
+
const field = this.findSavedField(event.detail.data?.fields || [])
|
|
77
|
+
if (!field) return
|
|
78
|
+
|
|
79
|
+
const attachment = field.attachments[0]
|
|
80
|
+
this.element.querySelector('[data-lato-cms-video-field-target="currentContainer"]')?.remove()
|
|
81
|
+
this.newPreviewContainerTarget.classList.add('d-none')
|
|
82
|
+
|
|
83
|
+
if (attachment) {
|
|
84
|
+
this.element.insertBefore(this.buildCurrentContainer(attachment), this.newPreviewContainerTarget)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
findSavedField(fields) {
|
|
89
|
+
return fields.find(field => {
|
|
90
|
+
return field.persisted_field_id === this.fieldId || field.field_id === this.fieldId
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
buildCurrentContainer(attachment) {
|
|
95
|
+
const container = document.createElement('div')
|
|
96
|
+
container.className = 'lato-cms-file-field__item lato-cms-video-field__item'
|
|
97
|
+
container.dataset.latoCmsVideoFieldTarget = 'currentContainer'
|
|
98
|
+
container.dataset.attachmentId = attachment.id
|
|
99
|
+
// poster_url is usually still null right after save: the poster is
|
|
100
|
+
// generated by a background job and shows up on the next page load.
|
|
101
|
+
const poster = attachment.poster_url ? ` poster="${attachment.poster_url}"` : ''
|
|
102
|
+
container.innerHTML = `
|
|
103
|
+
<div class="lato-cms-video-field__preview">
|
|
104
|
+
<video controls preload="metadata" class="lato-cms-video-field__player" src="${attachment.url}"${poster} data-lato-cms-video-field-target="currentVideo"></video>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="lato-cms-file-field__info">
|
|
107
|
+
<i class="bi bi-film"></i>
|
|
108
|
+
<span>${this.escapeHtml(attachment.filename)}</span>
|
|
109
|
+
<span class="text-muted small">(${this.humanSize(attachment.byte_size || 0)})</span>
|
|
110
|
+
</div>
|
|
111
|
+
<button type="button" class="lato-cms-attachment-field__remove" title="${this.removeLabel}" aria-label="${this.removeLabel}" data-action="lato-cms-video-field#remove">
|
|
112
|
+
<i class="bi bi-trash"></i>
|
|
113
|
+
</button>
|
|
114
|
+
<div class="lato-cms-file-field__removed d-none" data-lato-cms-video-field-target="removedNotice">
|
|
115
|
+
<span class="text-danger small">
|
|
116
|
+
<i class="bi bi-trash me-1"></i>${this.removePendingLabel}
|
|
117
|
+
</span>
|
|
118
|
+
<button type="button" class="btn btn-link btn-sm p-0" data-action="lato-cms-video-field#undo">
|
|
119
|
+
${this.undoLabel}
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
`
|
|
123
|
+
return container
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
revokeObjectUrl() {
|
|
127
|
+
if (!this.objectUrl) return
|
|
128
|
+
|
|
129
|
+
URL.revokeObjectURL(this.objectUrl)
|
|
130
|
+
this.objectUrl = null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
humanSize(size) {
|
|
134
|
+
if (size < 1024) return `${size} B`
|
|
135
|
+
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
|
136
|
+
return `${(size / 1024 / 1024).toFixed(1)} MB`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
escapeHtml(value) {
|
|
140
|
+
return value.replace(/[&<>"']/g, char => ({
|
|
141
|
+
'&': '&',
|
|
142
|
+
'<': '<',
|
|
143
|
+
'>': '>',
|
|
144
|
+
'"': '"',
|
|
145
|
+
"'": '''
|
|
146
|
+
}[char]))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
get fieldId() {
|
|
150
|
+
return this.element.dataset.fieldId
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
get removeLabel() {
|
|
154
|
+
return this.element.dataset.removeLabel || 'Remove'
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
get removePendingLabel() {
|
|
158
|
+
return this.element.dataset.removePendingLabel || 'Video will be removed on save'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
get undoLabel() {
|
|
162
|
+
return this.element.dataset.undoLabel || 'Undo'
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -268,6 +268,7 @@ body.controller-pages.action-show {
|
|
|
268
268
|
|
|
269
269
|
.lato-cms-file-field__info,
|
|
270
270
|
.lato-cms-image-field__preview,
|
|
271
|
+
.lato-cms-video-field__preview,
|
|
271
272
|
.lato-cms-attachment-field__remove,
|
|
272
273
|
> .btn {
|
|
273
274
|
display: none;
|
|
@@ -339,6 +340,27 @@ body.controller-pages.action-show {
|
|
|
339
340
|
font-size: 10px;
|
|
340
341
|
}
|
|
341
342
|
|
|
343
|
+
// ── Video field ────────────────────────────────────────────────────────────────
|
|
344
|
+
.lato-cms-video-field__item {
|
|
345
|
+
align-items: stretch;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.lato-cms-video-field__preview {
|
|
349
|
+
align-self: center;
|
|
350
|
+
background: #000;
|
|
351
|
+
border: 1px solid rgba(33, 37, 41, 0.08);
|
|
352
|
+
border-radius: 6px;
|
|
353
|
+
flex: 0 0 auto;
|
|
354
|
+
overflow: hidden;
|
|
355
|
+
width: 180px;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.lato-cms-video-field__player {
|
|
359
|
+
display: block;
|
|
360
|
+
max-height: 110px;
|
|
361
|
+
width: 100%;
|
|
362
|
+
}
|
|
363
|
+
|
|
342
364
|
// ── Gallery field ──────────────────────────────────────────────────────────────
|
|
343
365
|
.lato-cms-gallery-field__grid {
|
|
344
366
|
display: flex;
|
|
@@ -233,6 +233,19 @@ module LatoCms
|
|
|
233
233
|
component_id = template_component[:component_id]
|
|
234
234
|
item_ids = Array(repeater_order).reject(&:blank?).map(&:to_s)
|
|
235
235
|
|
|
236
|
+
# Enforce min/max item count (authoritative check). Missing settings = no constraint.
|
|
237
|
+
settings = template_component[:settings] || {}
|
|
238
|
+
min = settings['min'].presence && settings['min'].to_i
|
|
239
|
+
max = settings['max'].presence && settings['max'].to_i
|
|
240
|
+
if min && item_ids.size < min
|
|
241
|
+
errors << { field_id: template_component_id, errors: [t('lato_cms.repeater_min_error', count: min)] }
|
|
242
|
+
return
|
|
243
|
+
end
|
|
244
|
+
if max && item_ids.size > max
|
|
245
|
+
errors << { field_id: template_component_id, errors: [t('lato_cms.repeater_max_error', count: max)] }
|
|
246
|
+
return
|
|
247
|
+
end
|
|
248
|
+
|
|
236
249
|
order_field = @page.fields.find_or_initialize_by(
|
|
237
250
|
template_id: @page.template_id,
|
|
238
251
|
template_component_id: template_component_id,
|
|
@@ -286,6 +299,9 @@ module LatoCms
|
|
|
286
299
|
when 'image'
|
|
287
300
|
field.save if field.new_record?
|
|
288
301
|
replace_field_image(field, field_data)
|
|
302
|
+
when 'video'
|
|
303
|
+
field.save if field.new_record?
|
|
304
|
+
replace_field_video(field, field_data)
|
|
289
305
|
when 'gallery'
|
|
290
306
|
field.save if field.new_record?
|
|
291
307
|
attach_field_files(field, field_data)
|
|
@@ -341,6 +357,22 @@ module LatoCms
|
|
|
341
357
|
end
|
|
342
358
|
end
|
|
343
359
|
|
|
360
|
+
# Single-video semantics: uploading a new video replaces both the previous
|
|
361
|
+
# video and its auto-generated poster; removing the video drops the poster
|
|
362
|
+
# too. Poster generation runs in a background job to keep the save fast.
|
|
363
|
+
def replace_field_video(field, field_data)
|
|
364
|
+
if field_data[:files].present?
|
|
365
|
+
new_file = Array(field_data[:files]).compact.first
|
|
366
|
+
return unless new_file
|
|
367
|
+
|
|
368
|
+
field.files.each(&:purge)
|
|
369
|
+
field.files.attach(new_file)
|
|
370
|
+
LatoCms::GenerateVideoPosterJob.perform_later(field.id)
|
|
371
|
+
elsif field_data[:remove_file_ids].present?
|
|
372
|
+
field.files.each(&:purge)
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
344
376
|
def create_params
|
|
345
377
|
params.require(:page).permit(:title, :locale)
|
|
346
378
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module LatoCms
|
|
2
2
|
module PagesHelper
|
|
3
|
-
BUILTIN_FIELD_TYPES = %w[string textarea text number date datetime boolean select multiselect color json file image gallery].freeze
|
|
3
|
+
BUILTIN_FIELD_TYPES = %w[string textarea text number date datetime boolean select multiselect color json file image video gallery].freeze
|
|
4
4
|
|
|
5
5
|
# Returns the path for an Active Storage attachment.
|
|
6
6
|
# Using main_app avoids the missing `attachment_path` error inside the engine.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module LatoCms
|
|
2
|
+
# Generates the poster image for a video field in the background so the save
|
|
3
|
+
# request is not blocked by ffmpeg processing. Failures are swallowed by the
|
|
4
|
+
# model (logged as warnings): a missing poster must never break the field.
|
|
5
|
+
class GenerateVideoPosterJob < ApplicationJob
|
|
6
|
+
queue_as :default
|
|
7
|
+
|
|
8
|
+
def perform(page_field_id)
|
|
9
|
+
LatoCms::PageField.find_by(id: page_field_id)&.generate_video_poster!
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -75,6 +75,38 @@ module LatoCms
|
|
|
75
75
|
field_config&.dig('settings') || {}
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# A video field stores two attachments in `files`: the video itself and an
|
|
79
|
+
# optional auto-generated poster image. Content type is the discriminator,
|
|
80
|
+
# so no extra column or naming convention is needed.
|
|
81
|
+
def video_attachment
|
|
82
|
+
files.find { |file| file.content_type.to_s.start_with?("video/") }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def poster_attachment
|
|
86
|
+
files.find { |file| file.content_type.to_s.start_with?("image/") }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Generates a poster image from the video via Active Storage previews
|
|
90
|
+
# (ffmpeg) and attaches it alongside the video. Best effort: any failure is
|
|
91
|
+
# logged and the video keeps working without a poster.
|
|
92
|
+
def generate_video_poster!
|
|
93
|
+
video = video_attachment
|
|
94
|
+
return unless video
|
|
95
|
+
return if poster_attachment
|
|
96
|
+
|
|
97
|
+
unless video.previewable?
|
|
98
|
+
Rails.logger.warn("LatoCms: video preview unavailable (ffmpeg missing?) for attachment #{video.id}, skipping poster generation")
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
preview = video.preview(resize_to_limit: [1280, 720]).processed
|
|
103
|
+
preview.image.blob.open do |file|
|
|
104
|
+
files.attach(io: file, filename: "#{video.filename.base}_poster.jpg", content_type: preview.image.blob.content_type)
|
|
105
|
+
end
|
|
106
|
+
rescue StandardError => e
|
|
107
|
+
Rails.logger.warn("LatoCms: failed to generate video poster for field #{id}: #{e.message}")
|
|
108
|
+
end
|
|
109
|
+
|
|
78
110
|
def as_json(_options = {})
|
|
79
111
|
result = {
|
|
80
112
|
id: id,
|
|
@@ -93,6 +125,9 @@ module LatoCms
|
|
|
93
125
|
when 'image'
|
|
94
126
|
attached = files.first
|
|
95
127
|
result[:attachments] = attached ? [attachment_as_json(attached, with_variants: true)] : []
|
|
128
|
+
when 'video'
|
|
129
|
+
attached = video_attachment
|
|
130
|
+
result[:attachments] = attached ? [video_attachment_as_json(attached)] : []
|
|
96
131
|
when 'gallery'
|
|
97
132
|
order = value ? (JSON.parse(value) rescue []) : []
|
|
98
133
|
all_files = files.to_a
|
|
@@ -119,6 +154,15 @@ module LatoCms
|
|
|
119
154
|
json
|
|
120
155
|
end
|
|
121
156
|
|
|
157
|
+
# Video attachment json gains a `poster_url` key (nil until the poster job
|
|
158
|
+
# runs) so API consumers get video and poster URLs in a single object.
|
|
159
|
+
def video_attachment_as_json(attachment)
|
|
160
|
+
poster = poster_attachment
|
|
161
|
+
attachment_as_json(attachment).merge(
|
|
162
|
+
poster_url: poster && Rails.application.routes.url_helpers.rails_blob_path(poster, only_path: true)
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
122
166
|
# Builds a map of { size_name => variant_url } from the field's `settings.sizes`.
|
|
123
167
|
# Variants are processed lazily by Active Storage on first request to their URL.
|
|
124
168
|
def attachment_variant_urls(attachment)
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
<% saved_order = begin; order_field&.value.present? ? JSON.parse(order_field.value) : []; rescue JSON::ParserError; []; end %>
|
|
3
3
|
<% fields_by_item = component_fields.reject(&:repeater_order?).group_by(&:repeater_item_id) %>
|
|
4
4
|
<% item_ids = (saved_order.presence || fields_by_item.keys.compact) %>
|
|
5
|
-
<%
|
|
5
|
+
<% min = tc[:settings]['min'].to_i %>
|
|
6
|
+
<% max = tc[:settings]['max'].to_i %>
|
|
7
|
+
<%# Seed `min` empty items when the list is empty, so the minimum is met up front. %>
|
|
8
|
+
<% item_ids = Array.new(min) { SecureRandom.uuid } if item_ids.blank? && min.positive? %>
|
|
6
9
|
|
|
7
|
-
<div data-controller="lato-cms-repeater"
|
|
10
|
+
<div data-controller="lato-cms-repeater"
|
|
11
|
+
data-lato-cms-repeater-min-value="<%= min %>"
|
|
12
|
+
data-lato-cms-repeater-max-value="<%= max %>">
|
|
8
13
|
<div data-lato-cms-repeater-target="items">
|
|
9
14
|
<% item_ids.each_with_index do |item_id, item_index| %>
|
|
10
15
|
<% item_fields = (fields_by_item[item_id] || []).index_by(&:base_field_id) %>
|
|
@@ -16,7 +21,7 @@
|
|
|
16
21
|
<%= render 'lato_cms/pages/repeater_item', tc: tc, item_id: 'NEW_RECORD', item_index: 'NEW_INDEX', item_fields: {} %>
|
|
17
22
|
</template>
|
|
18
23
|
|
|
19
|
-
<button type="button" class="btn btn-sm btn-outline-primary" data-action="lato-cms-repeater#add">
|
|
24
|
+
<button type="button" class="btn btn-sm btn-outline-primary" data-action="lato-cms-repeater#add" data-lato-cms-repeater-target="addButton">
|
|
20
25
|
<i class="bi bi-plus-lg me-1"></i><%= t('lato_cms.repeater_add_item') %>
|
|
21
26
|
</button>
|
|
22
27
|
</div>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<% label = field_config['name'] || field_id.humanize %>
|
|
2
|
+
<% required = field_config['required'] == true %>
|
|
3
|
+
<% settings = field_config['settings'] || {} %>
|
|
4
|
+
<% accept = settings['accept'] || 'video/*' %>
|
|
5
|
+
<% current_file = page_field&.video_attachment %>
|
|
6
|
+
<% poster_file = page_field&.poster_attachment %>
|
|
7
|
+
<% files_input_name = lato_cms_field_input_name(field_id, 'files', input_name_prefix: local_assigns[:input_name_prefix], multiple: true) %>
|
|
8
|
+
<% remove_input_name = lato_cms_field_input_name(field_id, 'remove_file_ids', input_name_prefix: local_assigns[:input_name_prefix], multiple: true) %>
|
|
9
|
+
<% input_id = lato_cms_field_dom_id(field_id, 'files', dom_id_prefix: local_assigns[:dom_id_prefix]) %>
|
|
10
|
+
|
|
11
|
+
<label class="form-label">
|
|
12
|
+
<%= label %><%= ' *' if required %>
|
|
13
|
+
</label>
|
|
14
|
+
|
|
15
|
+
<div data-controller="lato-cms-video-field"
|
|
16
|
+
data-field-id="<%= field_id %>"
|
|
17
|
+
data-new-badge-label="<%= t('lato_cms.field_video_new_badge') %>"
|
|
18
|
+
data-remove-label="<%= t('lato_cms.field_video_remove') %>"
|
|
19
|
+
data-remove-pending-label="<%= t('lato_cms.field_video_remove_pending') %>"
|
|
20
|
+
data-undo-label="<%= t('lato_cms.field_file_remove_undo') %>">
|
|
21
|
+
<% if current_file %>
|
|
22
|
+
<div class="lato-cms-file-field__item lato-cms-video-field__item" data-lato-cms-video-field-target="currentContainer" data-attachment-id="<%= current_file.id %>">
|
|
23
|
+
<div class="lato-cms-video-field__preview">
|
|
24
|
+
<video controls preload="metadata" class="lato-cms-video-field__player" src="<%= lato_cms_attachment_path(current_file) %>" <% if poster_file %>poster="<%= lato_cms_attachment_path(poster_file) %>"<% end %> data-lato-cms-video-field-target="currentVideo"></video>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="lato-cms-file-field__info">
|
|
27
|
+
<i class="bi bi-film"></i>
|
|
28
|
+
<span><%= current_file.blob.filename %></span>
|
|
29
|
+
<span class="text-muted small">(<%= number_to_human_size(current_file.blob.byte_size) %>)</span>
|
|
30
|
+
</div>
|
|
31
|
+
<button type="button" class="lato-cms-attachment-field__remove" title="<%= t('lato_cms.field_video_remove') %>" aria-label="<%= t('lato_cms.field_video_remove') %>" data-action="lato-cms-video-field#remove">
|
|
32
|
+
<i class="bi bi-trash"></i>
|
|
33
|
+
</button>
|
|
34
|
+
<div class="lato-cms-file-field__removed d-none" data-lato-cms-video-field-target="removedNotice">
|
|
35
|
+
<span class="text-danger small">
|
|
36
|
+
<i class="bi bi-trash me-1"></i><%= t('lato_cms.field_video_remove_pending') %>
|
|
37
|
+
</span>
|
|
38
|
+
<button type="button" class="btn btn-link btn-sm p-0" data-action="lato-cms-video-field#undo">
|
|
39
|
+
<%= t('lato_cms.field_file_remove_undo') %>
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
|
|
45
|
+
<div class="lato-cms-file-field__item lato-cms-file-field__item--new lato-cms-video-field__item d-none" data-lato-cms-video-field-target="newPreviewContainer">
|
|
46
|
+
<div class="lato-cms-video-field__preview">
|
|
47
|
+
<video controls preload="metadata" class="lato-cms-video-field__player" data-lato-cms-video-field-target="newPreview"></video>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="lato-cms-file-field__info">
|
|
50
|
+
<i class="bi bi-film"></i>
|
|
51
|
+
<span data-lato-cms-video-field-target="newPreviewName"></span>
|
|
52
|
+
<span class="text-muted small" data-lato-cms-video-field-target="newPreviewSize"></span>
|
|
53
|
+
<span class="badge bg-success"><%= t('lato_cms.field_video_new_badge') %></span>
|
|
54
|
+
</div>
|
|
55
|
+
<button type="button" class="lato-cms-attachment-field__remove" title="<%= t('lato_cms.field_video_remove') %>" aria-label="<%= t('lato_cms.field_video_remove') %>" data-action="lato-cms-video-field#removeNew">
|
|
56
|
+
<i class="bi bi-trash"></i>
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<input type="file"
|
|
61
|
+
class="form-control"
|
|
62
|
+
name="<%= files_input_name %>"
|
|
63
|
+
id="<%= input_id %>"
|
|
64
|
+
accept="<%= accept %>"
|
|
65
|
+
data-lato-cms-video-field-target="input"
|
|
66
|
+
data-action="change->lato-cms-video-field#preview"
|
|
67
|
+
<%= 'required' if required && !current_file %>>
|
|
68
|
+
</div>
|
data/config/locales/en.yml
CHANGED
|
@@ -80,6 +80,8 @@ en:
|
|
|
80
80
|
fields_save_component: Save
|
|
81
81
|
repeater_add_item: Add item
|
|
82
82
|
repeater_remove_item: Remove
|
|
83
|
+
repeater_min_error: "At least %{count} items are required"
|
|
84
|
+
repeater_max_error: "At most %{count} items are allowed"
|
|
83
85
|
fields_component_missing_alert_html: "Component <strong>%{component_id}</strong> not found. The component configuration may have been removed."
|
|
84
86
|
fields_previously_saved_data: "Previously saved data:"
|
|
85
87
|
field_file_remove: Remove
|
|
@@ -92,6 +94,9 @@ en:
|
|
|
92
94
|
field_image_remove: Remove
|
|
93
95
|
field_image_remove_pending: Image will be removed on save
|
|
94
96
|
field_image_new_badge: New
|
|
97
|
+
field_video_remove: Remove
|
|
98
|
+
field_video_remove_pending: Video will be removed on save
|
|
99
|
+
field_video_new_badge: New
|
|
95
100
|
field_multiselect_hint: Hold Ctrl/Cmd to select multiple
|
|
96
101
|
field_select_placeholder: "-- Select --"
|
|
97
102
|
field_text_toolbar_bold: Bold
|
data/config/locales/it.yml
CHANGED
|
@@ -80,6 +80,8 @@ it:
|
|
|
80
80
|
fields_save_component: Salva
|
|
81
81
|
repeater_add_item: Aggiungi elemento
|
|
82
82
|
repeater_remove_item: Rimuovi
|
|
83
|
+
repeater_min_error: "Sono richiesti almeno %{count} elementi"
|
|
84
|
+
repeater_max_error: "Sono ammessi al massimo %{count} elementi"
|
|
83
85
|
fields_component_missing_alert_html: "Componente <strong>%{component_id}</strong> non trovato. La configurazione del componente potrebbe essere stata rimossa."
|
|
84
86
|
fields_previously_saved_data: "Dati salvati in precedenza:"
|
|
85
87
|
field_file_remove: Rimuovi
|
|
@@ -92,6 +94,9 @@ it:
|
|
|
92
94
|
field_image_remove: Rimuovi
|
|
93
95
|
field_image_remove_pending: L'immagine verra rimossa al salvataggio
|
|
94
96
|
field_image_new_badge: Nuovo
|
|
97
|
+
field_video_remove: Rimuovi
|
|
98
|
+
field_video_remove_pending: Il video verra rimosso al salvataggio
|
|
99
|
+
field_video_new_badge: Nuovo
|
|
95
100
|
field_multiselect_hint: Tieni premuto Ctrl/Cmd per selezionare piu opzioni
|
|
96
101
|
field_select_placeholder: "-- Seleziona --"
|
|
97
102
|
field_text_toolbar_bold: Grassetto
|
data/lib/lato_cms/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lato_cms
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.0.
|
|
4
|
+
version: 3.0.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gregorio Galante
|
|
@@ -103,6 +103,7 @@ files:
|
|
|
103
103
|
- app/assets/javascripts/lato_cms/controllers/lato_cms_page_preview_controller.js
|
|
104
104
|
- app/assets/javascripts/lato_cms/controllers/lato_cms_repeater_controller.js
|
|
105
105
|
- app/assets/javascripts/lato_cms/controllers/lato_cms_text_field_controller.js
|
|
106
|
+
- app/assets/javascripts/lato_cms/controllers/lato_cms_video_field_controller.js
|
|
106
107
|
- app/assets/stylesheets/lato_cms/application.scss
|
|
107
108
|
- app/controllers/lato_cms/api/pages_controller.rb
|
|
108
109
|
- app/controllers/lato_cms/application_controller.rb
|
|
@@ -110,6 +111,7 @@ files:
|
|
|
110
111
|
- app/helpers/lato_cms/application_helper.rb
|
|
111
112
|
- app/helpers/lato_cms/pages_helper.rb
|
|
112
113
|
- app/jobs/lato_cms/application_job.rb
|
|
114
|
+
- app/jobs/lato_cms/generate_video_poster_job.rb
|
|
113
115
|
- app/mailers/lato_cms/application_mailer.rb
|
|
114
116
|
- app/models/lato_cms/page.rb
|
|
115
117
|
- app/models/lato_cms/page_field.rb
|
|
@@ -136,6 +138,7 @@ files:
|
|
|
136
138
|
- app/views/lato_cms/pages/fields/_string.html.erb
|
|
137
139
|
- app/views/lato_cms/pages/fields/_text.html.erb
|
|
138
140
|
- app/views/lato_cms/pages/fields/_textarea.html.erb
|
|
141
|
+
- app/views/lato_cms/pages/fields/_video.html.erb
|
|
139
142
|
- app/views/lato_cms/pages/index.html.erb
|
|
140
143
|
- app/views/lato_cms/pages/show.html.erb
|
|
141
144
|
- app/views/lato_cms/pages/translations.html.erb
|