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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f424fc472d1089710195d8c872e462f4418c010fb452725d7f566ea7bd8b475
4
- data.tar.gz: 5610d6b09ba78c999ea26e0acddf49855e836fca77c5ffc00438064f43cf4b91
3
+ metadata.gz: a16695350bb974f347a29439898cade24c9447d2fa239468fd9309165295c41c
4
+ data.tar.gz: ba68397002e0adc6d2f07d36f574157d15a75ac817620a172b6885c7541411c4
5
5
  SHA512:
6
- metadata.gz: df88eb580c7e43b08baec394249349e540dbc7ffdd1b0ef170db037ede14368a8d3f876e1a4333f41a6b61f7ee8e746290ece67b335d2f5844be80f2b6637b93
7
- data.tar.gz: b3c24f61a461e6fc47eb8a2d6605eb42c73d3fb9be81946d31d5257404140874aacde6360ea0de1066ce69f6c60a843239cfed23a77792c7641ed17547da8427
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"
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Lato
2
- VERSION = "3.14.9"
2
+ VERSION = "3.15.0"
3
3
  end
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.14.9
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-16 00:00:00.000000000 Z
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