Skip to content

Commit e1a86c9

Browse files
authored
Merge pull request #4143 from AlchemyCMS/uploader-progress-in-dropzone
fix(uploader): render upload progress inside the dropzone
2 parents 52f949f + ef91220 commit e1a86c9

12 files changed

Lines changed: 122 additions & 9 deletions

File tree

app/assets/builds/alchemy/admin.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/assets/builds/alchemy/alchemy_admin.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/assets/builds/alchemy/alchemy_admin.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/components/alchemy/admin/uploader_button.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,21 @@ module Admin
33
class UploaderButton < ViewComponent::Base
44
delegate :alchemy, to: :helpers
55

6-
attr_reader :object, :file_attribute, :accept, :dropzone, :redirect_url, :inline_label
6+
attr_reader :object, :file_attribute, :accept, :dropzone, :redirect_url, :layout, :inline_label
77

8-
def initialize(object:, file_attribute:, redirect_url:, accept: nil, dropzone: nil, label: nil, inline_label: false)
8+
def initialize(object:, file_attribute:, redirect_url:, accept: nil, dropzone: nil, label: nil, layout: nil, inline_label: false)
99
@object = object
1010
@file_attribute = file_attribute
1111
@redirect_url = redirect_url
1212
@dropzone = dropzone || "#main_content"
1313
@label = label
14+
@layout = layout
1415
@accept = accept || ((file_types.to_a == ["*"]) ? nil : file_types.map { |type| ".#{type}" }.join(", "))
1516
@inline_label = inline_label
1617
end
1718

1819
def call
19-
content_tag "alchemy-uploader", "redirect-url": redirect_url, dropzone: do
20+
content_tag "alchemy-uploader", "redirect-url": redirect_url, dropzone:, layout: do
2021
form_for [alchemy, :admin, object], html: {multipart: true, class: "upload-button"} do |f|
2122
safe_join([upload_hash_field(f), file_input(f), upload_label(f)].compact)
2223
end

app/javascript/alchemy_admin/components/uploader.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,23 +154,37 @@ export class Uploader extends HTMLElement {
154154
#createProgress(fileUploads) {
155155
if (this.uploadProgress) {
156156
this.uploadProgress.cancel()
157-
document.body.removeChild(this.uploadProgress)
157+
this.uploadProgress.remove()
158158
}
159159
this.uploadProgress = new Progress()
160160
this.uploadProgress.initialize(fileUploads)
161+
this.uploadProgress.setAttribute("layout", this.layout)
161162
this.uploadProgress.onComplete = (status) => {
162163
this.dispatchEvent(
163164
new CustomEvent(`Alchemy.upload.${status}`, { bubbles: true })
164165
)
165166
}
166167

167-
document.body.append(this.uploadProgress)
168+
this.#progressContainer.append(this.uploadProgress)
169+
}
170+
171+
// The dropzone doubles as the progress container so the upload indicators
172+
// render inside the archive (and inside dialogs). Falls back to the body for
173+
// uploaders without a dropzone (e.g. the attachment replace button).
174+
get #progressContainer() {
175+
return this.#dropzoneElement ?? document.body
168176
}
169177

170178
get dropzone() {
171179
return this.getAttribute("dropzone")
172180
}
173181

182+
// Shape of the upload indicators. Defaults to "row" so uploads fit naturally
183+
// into resource table views; set layout="card" for the thumbnail grid.
184+
get layout() {
185+
return this.getAttribute("layout") || "row"
186+
}
187+
174188
/**
175189
* @returns {HTMLInputElement}
176190
*/

app/stylesheets/alchemy/admin/frame.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ div#overlay_text_box {
9696
padding-right: var(--spacing-2);
9797
padding-bottom: var(--spacing-2);
9898
padding-left: calc(var(--main-menu-width) + var(--spacing-2));
99+
position: relative;
99100
z-index: 0;
100101
width: 100%;
101102
height: 100%;

app/stylesheets/alchemy/admin/upload.scss

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@
4141
}
4242
}
4343

44+
// #main_content scrolls, so the absolute drop indicator (pinned to the top of
45+
// the scrollable content) scrolls out of view. Pin it to the visible viewport
46+
// area instead, inset past the top and main menu.
47+
#main_content.dragover::after {
48+
position: fixed;
49+
inset: var(--top-menu-height) 0 0 var(--main-menu-width);
50+
width: auto;
51+
height: auto;
52+
}
53+
4454
alchemy-upload-progress {
4555
--border-radius: var(--border-radius_medium);
4656
--pogress_value-width: calc(232px - var(--spacing-2));
@@ -55,9 +65,13 @@ alchemy-upload-progress {
5565
);
5666
height: auto;
5767
left: 0;
68+
max-height: 100%;
5869
opacity: 0;
70+
// Only scroll vertically; setting overflow-y alone forces overflow-x to auto,
71+
// which shows a spurious horizontal scrollbar for the grid's sub-pixel overflow.
72+
overflow: hidden auto;
5973
padding: var(--spacing-4);
60-
position: fixed;
74+
position: absolute;
6175
transition: var(--transition-duration) ease-in-out;
6276
width: 100%;
6377
z-index: 150;
@@ -161,6 +175,52 @@ alchemy-upload-progress {
161175
}
162176
}
163177

178+
// Inside the scrolling, menu-padded #main_content (a z-index:0 stacking context
179+
// below the fixed main menu) an absolutely positioned progress hides behind the
180+
// menu and scrolls away. Pin it to the viewport, inset past the main menu.
181+
#main_content > alchemy-upload-progress {
182+
position: fixed;
183+
left: var(--main-menu-width);
184+
right: 0;
185+
width: auto;
186+
}
187+
188+
// Mimic the picture archive grid: each upload is a thumbnail-shaped card.
189+
alchemy-upload-progress[layout="card"] {
190+
.single-uploads {
191+
grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
192+
}
193+
194+
alchemy-file-upload {
195+
flex-direction: column;
196+
align-items: stretch;
197+
text-align: center;
198+
199+
img {
200+
height: 120px;
201+
width: 100%;
202+
}
203+
204+
.description {
205+
align-self: stretch;
206+
}
207+
208+
button {
209+
position: absolute;
210+
top: var(--spacing-1);
211+
right: var(--spacing-1);
212+
margin-left: 0;
213+
}
214+
}
215+
}
216+
217+
// Default: each upload is a stacked row, fitting resource table views.
218+
alchemy-upload-progress[layout="row"] {
219+
.single-uploads {
220+
grid-template-columns: 1fr;
221+
}
222+
}
223+
164224
.successful {
165225
--file-upload_progress-indicator-color: var(
166226
--file-upload_progress-indicator-color-successful

app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
dropzone: '#assign_image_list',
77
inline_label: true,
88
file_attribute: 'image_file',
9+
layout: 'card',
910
redirect_url: alchemy.admin_pictures_path(
1011
size: search_filter_params[:size],
1112
q: { last_upload: true },

app/views/alchemy/admin/pictures/index.html.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<%= render Alchemy::Admin::UploaderButton.new(
66
object: Alchemy::Picture.new,
77
file_attribute: 'image_file',
8+
layout: 'card',
89
redirect_url: alchemy.admin_pictures_path(
910
size: @size,
1011
q: { last_upload: true }

app/views/alchemy/admin/uploader/_button.html.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
redirect_url:,
88
accept: local_assigns[:accept],
99
dropzone: local_assigns[:dropzone],
10+
layout: local_assigns[:layout],
1011
label: local_assigns[:label]) %>

0 commit comments

Comments
 (0)