lato 3.14.9 → 3.15.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/controllers/lato_input_dropzone_controller.js +212 -0
- data/app/helpers/lato/components_helper.rb +6 -0
- data/app/views/lato/components/_form_item_input_file_dropzone.html.erb +48 -0
- data/app/views/lato/components/_index.html.erb +2 -2
- data/config/importmap.rb +2 -0
- data/config/locales/en.yml +1 -0
- data/config/locales/fr.yml +1 -0
- data/config/locales/it.yml +1 -0
- data/config/locales/ro.yml +1 -0
- data/lib/lato/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a16695350bb974f347a29439898cade24c9447d2fa239468fd9309165295c41c
|
|
4
|
+
data.tar.gz: ba68397002e0adc6d2f07d36f574157d15a75ac817620a172b6885c7541411c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cbd1f1c3aa108c5bd9ccccff548705a32946fe3978ee6a9d5af009a82d79229f75371882444c117c0e704ef5259bbb08e60c3476608bd2ec94506e9474a600c4
|
|
7
|
+
data.tar.gz: 14714a2de245b82ae0abbbeb1ba859c5b1ebc74af64fc00a511d002a5625904e833d2277a9333284ded22894310d2eb95d6aab6b803d8e506f72d1cbcd0fa188
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { DirectUpload } from "@rails/activestorage"
|
|
3
|
+
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static targets = ["input", "dropzone", "preview"]
|
|
6
|
+
static values = {
|
|
7
|
+
multiple: Boolean,
|
|
8
|
+
directUpload: Boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
connect() {
|
|
12
|
+
this.files = []
|
|
13
|
+
|
|
14
|
+
if (this.directUploadValue) {
|
|
15
|
+
this.inputName = this.inputTarget.name
|
|
16
|
+
this.inputTarget.removeAttribute('name')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Initialize files from input if any (e.g. browser back button)
|
|
20
|
+
if (this.inputTarget.files.length > 0) {
|
|
21
|
+
this.handleFiles(Array.from(this.inputTarget.files))
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
onClick(e) {
|
|
26
|
+
this.inputTarget.click()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
onDragOver(e) {
|
|
30
|
+
e.preventDefault()
|
|
31
|
+
this.dropzoneTarget.classList.add("opacity-75")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
onDragLeave(e) {
|
|
35
|
+
e.preventDefault()
|
|
36
|
+
this.dropzoneTarget.classList.remove("opacity-75")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
onDrop(e) {
|
|
40
|
+
e.preventDefault()
|
|
41
|
+
this.dropzoneTarget.classList.remove("opacity-75")
|
|
42
|
+
|
|
43
|
+
if (e.dataTransfer.files.length > 0) {
|
|
44
|
+
this.handleFiles(Array.from(e.dataTransfer.files))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
onInputChange(e) {
|
|
49
|
+
if (this.inputTarget.files.length > 0) {
|
|
50
|
+
// When input changes via dialog, we add these files.
|
|
51
|
+
// Note: standard input behavior replaces files.
|
|
52
|
+
// We treat the input selection as "new files to add" if multiple, or "replace" if single.
|
|
53
|
+
// But since we can't easily distinguish "add" vs "replace" intent from a simple change event on the same input,
|
|
54
|
+
// and the input itself only holds the new selection, we have to be careful.
|
|
55
|
+
|
|
56
|
+
// Strategy:
|
|
57
|
+
// 1. Read new files from input.
|
|
58
|
+
// 2. Update our internal list.
|
|
59
|
+
// 3. Update the input to reflect the FULL list (so form submit works for normal uploads).
|
|
60
|
+
|
|
61
|
+
const newFiles = Array.from(this.inputTarget.files)
|
|
62
|
+
this.handleFiles(newFiles)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
handleFiles(newFiles) {
|
|
67
|
+
if (this.multipleValue) {
|
|
68
|
+
// Filter duplicates if needed, or just add.
|
|
69
|
+
// For simplicity, just add.
|
|
70
|
+
this.files = [...this.files, ...newFiles]
|
|
71
|
+
} else {
|
|
72
|
+
this.files = newFiles
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.updateInput()
|
|
76
|
+
this.updatePreview()
|
|
77
|
+
|
|
78
|
+
if (this.directUploadValue) {
|
|
79
|
+
this.uploadFiles(newFiles)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
updateInput() {
|
|
84
|
+
const dataTransfer = new DataTransfer()
|
|
85
|
+
this.files.forEach(file => dataTransfer.items.add(file))
|
|
86
|
+
this.inputTarget.files = dataTransfer.files
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
updatePreview() {
|
|
90
|
+
this.previewTarget.innerHTML = ""
|
|
91
|
+
this.files.forEach((file, index) => {
|
|
92
|
+
const fileElement = document.createElement("div")
|
|
93
|
+
fileElement.classList.add("d-flex", "align-items-center", "justify-content-between", "p-2", "border", "rounded", "mb-2")
|
|
94
|
+
|
|
95
|
+
const info = document.createElement("div")
|
|
96
|
+
info.classList.add("d-flex", "align-items-center")
|
|
97
|
+
|
|
98
|
+
const icon = document.createElement("i")
|
|
99
|
+
icon.classList.add("bi", "bi-file-earmark", "fs-4", "me-2")
|
|
100
|
+
|
|
101
|
+
const name = document.createElement("span")
|
|
102
|
+
name.textContent = file.name
|
|
103
|
+
name.classList.add("text-truncate")
|
|
104
|
+
name.style.maxWidth = "200px"
|
|
105
|
+
|
|
106
|
+
info.appendChild(icon)
|
|
107
|
+
info.appendChild(name)
|
|
108
|
+
|
|
109
|
+
const rightSide = document.createElement("div")
|
|
110
|
+
rightSide.classList.add("d-flex", "align-items-center")
|
|
111
|
+
|
|
112
|
+
// Progress bar container
|
|
113
|
+
if (this.directUploadValue) {
|
|
114
|
+
const progressContainer = document.createElement("div")
|
|
115
|
+
progressContainer.classList.add("me-3")
|
|
116
|
+
progressContainer.style.width = "100px"
|
|
117
|
+
progressContainer.id = `upload-progress-${this.getFileId(file)}`
|
|
118
|
+
progressContainer.style.display = "none"
|
|
119
|
+
|
|
120
|
+
const progressBar = document.createElement("div")
|
|
121
|
+
progressBar.classList.add("progress")
|
|
122
|
+
progressBar.style.height = "5px"
|
|
123
|
+
|
|
124
|
+
const progressBarInner = document.createElement("div")
|
|
125
|
+
progressBarInner.classList.add("progress-bar")
|
|
126
|
+
progressBarInner.role = "progressbar"
|
|
127
|
+
progressBarInner.style.width = "0%"
|
|
128
|
+
|
|
129
|
+
progressBar.appendChild(progressBarInner)
|
|
130
|
+
progressContainer.appendChild(progressBar)
|
|
131
|
+
rightSide.appendChild(progressContainer)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const removeBtn = document.createElement("button")
|
|
135
|
+
removeBtn.type = "button"
|
|
136
|
+
removeBtn.classList.add("btn", "btn-sm", "btn-outline-danger")
|
|
137
|
+
removeBtn.innerHTML = '<i class="bi bi-x"></i>'
|
|
138
|
+
removeBtn.onclick = (e) => {
|
|
139
|
+
e.stopPropagation()
|
|
140
|
+
this.removeFile(index)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
rightSide.appendChild(removeBtn)
|
|
144
|
+
fileElement.appendChild(info)
|
|
145
|
+
fileElement.appendChild(rightSide)
|
|
146
|
+
|
|
147
|
+
this.previewTarget.appendChild(fileElement)
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
removeFile(index) {
|
|
152
|
+
const file = this.files[index]
|
|
153
|
+
|
|
154
|
+
if (this.directUploadValue) {
|
|
155
|
+
const hiddenInput = this.element.querySelector(`input[type="hidden"][data-file-id="${this.getFileId(file)}"]`)
|
|
156
|
+
if (hiddenInput) hiddenInput.remove()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.files.splice(index, 1)
|
|
160
|
+
this.updateInput()
|
|
161
|
+
this.updatePreview()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
getFileId(file) {
|
|
165
|
+
return `${file.name.replace(/[^a-zA-Z0-9]/g, '')}-${file.lastModified}`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
uploadFiles(files) {
|
|
169
|
+
const url = this.inputTarget.dataset.directUploadUrl
|
|
170
|
+
if (!url) return
|
|
171
|
+
|
|
172
|
+
files.forEach(file => {
|
|
173
|
+
const progressId = `upload-progress-${this.getFileId(file)}`
|
|
174
|
+
|
|
175
|
+
const delegate = {
|
|
176
|
+
directUploadWillStoreFileWithXHR: (request) => {
|
|
177
|
+
request.upload.addEventListener("progress", (event) => {
|
|
178
|
+
const element = document.getElementById(progressId)
|
|
179
|
+
if (element) {
|
|
180
|
+
element.style.display = "block"
|
|
181
|
+
const progress = (event.loaded / event.total) * 100
|
|
182
|
+
element.querySelector(".progress-bar").style.width = `${progress}%`
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const upload = new DirectUpload(file, url, delegate)
|
|
189
|
+
|
|
190
|
+
upload.create((error, blob) => {
|
|
191
|
+
if (error) {
|
|
192
|
+
const element = document.getElementById(progressId)
|
|
193
|
+
if (element) {
|
|
194
|
+
element.querySelector(".progress-bar").classList.add("bg-danger")
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
const element = document.getElementById(progressId)
|
|
198
|
+
if (element) {
|
|
199
|
+
element.querySelector(".progress-bar").classList.add("bg-success")
|
|
200
|
+
|
|
201
|
+
const hiddenInput = document.createElement("input")
|
|
202
|
+
hiddenInput.type = "hidden"
|
|
203
|
+
hiddenInput.name = this.inputName
|
|
204
|
+
hiddenInput.value = blob.signed_id
|
|
205
|
+
hiddenInput.dataset.fileId = this.getFileId(file)
|
|
206
|
+
this.element.appendChild(hiddenInput)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -202,6 +202,12 @@ module Lato
|
|
|
202
202
|
form.file_field key, options
|
|
203
203
|
end
|
|
204
204
|
|
|
205
|
+
def lato_form_item_input_file_dropzone(form, key, options = {})
|
|
206
|
+
_lato_form_input_options(form, key, options, :change, '')
|
|
207
|
+
|
|
208
|
+
render 'lato/components/form_item_input_file_dropzone', form: form, key: key, options: options
|
|
209
|
+
end
|
|
210
|
+
|
|
205
211
|
def lato_form_item_input_textarea(form, key, options = {})
|
|
206
212
|
_lato_form_input_options(form, key, options, :keyup, 'form-control')
|
|
207
213
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<%
|
|
2
|
+
form ||= nil
|
|
3
|
+
key ||= nil
|
|
4
|
+
options ||= {}
|
|
5
|
+
|
|
6
|
+
multiple = options[:multiple] || false
|
|
7
|
+
direct_upload = options[:direct_upload] || false
|
|
8
|
+
|
|
9
|
+
# Merge classes
|
|
10
|
+
input_classes = options[:class] || []
|
|
11
|
+
input_classes = input_classes.is_a?(Array) ? input_classes : input_classes.split(' ')
|
|
12
|
+
input_classes.push('d-none')
|
|
13
|
+
|
|
14
|
+
# Merge data attributes
|
|
15
|
+
input_data = options[:data] || {}
|
|
16
|
+
input_data[:lato_input_dropzone_target] = 'input'
|
|
17
|
+
current_action = input_data[:action] || ''
|
|
18
|
+
input_data[:action] = "#{current_action} change->lato-input-dropzone#onInputChange".strip
|
|
19
|
+
|
|
20
|
+
input_options = options.merge(
|
|
21
|
+
class: input_classes,
|
|
22
|
+
data: input_data
|
|
23
|
+
)
|
|
24
|
+
%>
|
|
25
|
+
|
|
26
|
+
<div class="lato-input-dropzone mb-3"
|
|
27
|
+
data-controller="lato-input-dropzone"
|
|
28
|
+
data-lato-input-dropzone-multiple-value="<%= multiple %>"
|
|
29
|
+
data-lato-input-dropzone-direct-upload-value="<%= direct_upload %>">
|
|
30
|
+
|
|
31
|
+
<%= form.file_field key, input_options %>
|
|
32
|
+
|
|
33
|
+
<div class="border rounded p-4 text-center cursor-pointer position-relative"
|
|
34
|
+
style="border-style: dashed !important; border-width: 2px !important;"
|
|
35
|
+
data-lato-input-dropzone-target="dropzone"
|
|
36
|
+
data-action="click->lato-input-dropzone#onClick dragover->lato-input-dropzone#onDragOver dragleave->lato-input-dropzone#onDragLeave drop->lato-input-dropzone#onDrop">
|
|
37
|
+
|
|
38
|
+
<div class="text-muted pointer-events-none">
|
|
39
|
+
<i class="bi bi-cloud-arrow-up fs-1"></i>
|
|
40
|
+
<div class="mt-2">
|
|
41
|
+
<%= I18n.t('lato.dropzone_drag_and_drop_or_click') %>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="file-list mt-3" data-lato-input-dropzone-target="preview"></div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
@@ -27,7 +27,7 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
|
|
|
27
27
|
><i class="bi bi-search"></i></button>
|
|
28
28
|
</div>
|
|
29
29
|
<% else %>
|
|
30
|
-
<div></div>
|
|
30
|
+
<div class="w-100"></div>
|
|
31
31
|
<% end %>
|
|
32
32
|
|
|
33
33
|
<% if custom_actions.any? %>
|
|
@@ -105,7 +105,7 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
|
|
|
105
105
|
<% else %>
|
|
106
106
|
|
|
107
107
|
<div class="table-responsive">
|
|
108
|
-
<table class="table table-striped table-bordered">
|
|
108
|
+
<table class="table table-striped table-bordered mb-0">
|
|
109
109
|
<thead>
|
|
110
110
|
<tr class="align-middle">
|
|
111
111
|
<% columns.each do |column| %>
|
data/config/importmap.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
pin "lato/application", to: "lato/application.js"
|
|
2
2
|
pin_all_from Lato::Engine.root.join("app/assets/javascripts/lato/controllers"), under: "controllers", to: "lato/controllers"
|
|
3
3
|
|
|
4
|
+
pin "@rails/activestorage", to: "activestorage.esm.js", preload: true
|
|
5
|
+
|
|
4
6
|
pin "bootstrap", to: "bootstrap.js", preload: true
|
|
5
7
|
# pin "@popperjs/core", to: "popper.js", preload: true
|
|
6
8
|
pin "lodash", to: "https://ga.jspm.io/npm:lodash@4.17.21/lodash.js"
|
data/config/locales/en.yml
CHANGED
|
@@ -70,6 +70,7 @@ en:
|
|
|
70
70
|
operation_completed_subtitle: The operation has been completed successfully
|
|
71
71
|
operation_failed_title: Operation failed
|
|
72
72
|
operation_failed_subtitle: The operation has failed because of an error
|
|
73
|
+
dropzone_drag_and_drop_or_click: Drag and drop files here or click to upload
|
|
73
74
|
|
|
74
75
|
invitation_mailer:
|
|
75
76
|
invite_mail_subject: You received an invitation
|
data/config/locales/fr.yml
CHANGED
|
@@ -72,6 +72,7 @@ fr:
|
|
|
72
72
|
operation_completed_subtitle: L'opération a été réalisée avec succès
|
|
73
73
|
operation_failed_title: Échec de l'opération
|
|
74
74
|
operation_failed_subtitle: Une erreur s'est produite lors de l'opération
|
|
75
|
+
dropzone_drag_and_drop_or_click: Faites glisser et déposez les fichiers ici ou cliquez pour télécharger
|
|
75
76
|
|
|
76
77
|
invitation_mailer:
|
|
77
78
|
invite_mail_subject: Vous avez reçu une invitation
|
data/config/locales/it.yml
CHANGED
|
@@ -72,6 +72,7 @@ it:
|
|
|
72
72
|
operation_completed_subtitle: L'operazione è stata completata con successo
|
|
73
73
|
operation_failed_title: Operazione fallita
|
|
74
74
|
operation_failed_subtitle: Si è verificato un errore durante l'operazione
|
|
75
|
+
dropzone_drag_and_drop_or_click: Trascina qui i file o clicca per caricare
|
|
75
76
|
|
|
76
77
|
invitation_mailer:
|
|
77
78
|
invite_mail_subject: Hai ricevuto un invito
|
data/config/locales/ro.yml
CHANGED
|
@@ -72,6 +72,7 @@ ro:
|
|
|
72
72
|
operation_completed_subtitle: Operația a fost finalizată cu succes
|
|
73
73
|
operation_failed_title: Operație eșuată
|
|
74
74
|
operation_failed_subtitle: A apărut o eroare în timpul operației
|
|
75
|
+
dropzone_drag_and_drop_or_click: Trage și plasează fișiere aici sau fă clic pentru a le încărca
|
|
75
76
|
|
|
76
77
|
invitation_mailer:
|
|
77
78
|
invite_mail_subject: Ai primit o invitație
|
data/lib/lato/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lato
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gregorio Galante
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-11-
|
|
11
|
+
date: 2025-11-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -167,6 +167,7 @@ files:
|
|
|
167
167
|
- app/assets/javascripts/lato/controllers/lato_index_controller.js
|
|
168
168
|
- app/assets/javascripts/lato/controllers/lato_input_autocomplete2_controller.js
|
|
169
169
|
- app/assets/javascripts/lato/controllers/lato_input_autocomplete_controller.js
|
|
170
|
+
- app/assets/javascripts/lato/controllers/lato_input_dropzone_controller.js
|
|
170
171
|
- app/assets/javascripts/lato/controllers/lato_network_controller.js
|
|
171
172
|
- app/assets/javascripts/lato/controllers/lato_operation_controller.js
|
|
172
173
|
- app/assets/javascripts/lato/controllers/lato_tooltip_controller.js
|
|
@@ -222,6 +223,7 @@ files:
|
|
|
222
223
|
- app/views/lato/authentication/update_password.html.erb
|
|
223
224
|
- app/views/lato/authentication/verify_email.html.erb
|
|
224
225
|
- app/views/lato/authentication/web3_signin.html.erb
|
|
226
|
+
- app/views/lato/components/_form_item_input_file_dropzone.html.erb
|
|
225
227
|
- app/views/lato/components/_index.html.erb
|
|
226
228
|
- app/views/lato/components/_navbar_nav_item.html.erb
|
|
227
229
|
- app/views/lato/components/_navbar_nav_locales_item.html.erb
|