alchemy_cms 8.2.7 → 8.3.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/README.md +4 -1
- data/app/assets/builds/alchemy/admin.css +1 -1
- data/app/assets/builds/alchemy/alchemy_admin.min.js +1 -1
- data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -1
- data/app/assets/builds/alchemy/dark-theme.css +1 -1
- data/app/assets/builds/alchemy/light-theme.css +1 -1
- data/app/assets/builds/alchemy/preview.min.js +1 -1
- data/app/assets/builds/alchemy/theme.css +1 -1
- data/app/assets/builds/alchemy/welcome.css +1 -1
- data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
- data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -1
- data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
- data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -1
- data/app/assets/images/alchemy/admin/logo.svg +27 -0
- data/app/assets/images/alchemy/icons-sprite.svg +1 -1
- data/app/components/alchemy/admin/dashboard/widget.rb +40 -0
- data/app/components/alchemy/admin/dashboard/widgets/attachment_counts.rb +17 -0
- data/app/components/alchemy/admin/dashboard/widgets/element_usage.rb +37 -0
- data/app/components/alchemy/admin/dashboard/widgets/greeting.html.erb +13 -0
- data/app/components/alchemy/admin/dashboard/widgets/greeting.rb +21 -0
- data/app/components/alchemy/admin/dashboard/widgets/locked_pages.html.erb +54 -0
- data/app/components/alchemy/admin/dashboard/widgets/locked_pages.rb +20 -0
- data/app/components/alchemy/admin/dashboard/widgets/online_users.html.erb +22 -0
- data/app/components/alchemy/admin/dashboard/widgets/online_users.rb +19 -0
- data/app/components/alchemy/admin/dashboard/widgets/page_counts.rb +23 -0
- data/app/components/alchemy/admin/dashboard/widgets/page_usage.rb +46 -0
- data/app/components/alchemy/admin/dashboard/widgets/picture_counts.rb +17 -0
- data/app/components/alchemy/admin/dashboard/widgets/recent_pages.html.erb +41 -0
- data/app/components/alchemy/admin/dashboard/widgets/recent_pages.rb +16 -0
- data/app/components/alchemy/admin/dashboard/widgets/sites.html.erb +29 -0
- data/app/components/alchemy/admin/dashboard/widgets/sites.rb +15 -0
- data/app/components/alchemy/admin/dashboard/widgets/stat_widget.html.erb +23 -0
- data/app/components/alchemy/admin/dashboard/widgets/stat_widget.rb +19 -0
- data/app/components/alchemy/admin/dashboard/widgets/system_info.html.erb +32 -0
- data/app/components/alchemy/admin/dashboard/widgets/system_info.rb +37 -0
- data/app/components/alchemy/admin/dashboard/widgets/usage_widget.html.erb +42 -0
- data/app/components/alchemy/admin/dashboard/widgets/usage_widget.rb +66 -0
- data/app/components/alchemy/admin/dashboard/widgets/user_counts.rb +25 -0
- data/app/components/alchemy/admin/element_editor.html.erb +27 -20
- data/app/components/alchemy/admin/element_schedule_timestamps.rb +33 -0
- data/app/components/alchemy/admin/element_select.rb +4 -3
- data/app/components/alchemy/admin/page_node.html.erb +1 -20
- data/app/components/alchemy/admin/page_publication_fields.html.erb +30 -0
- data/app/components/alchemy/admin/page_publication_fields.rb +18 -0
- data/app/components/alchemy/admin/page_status_indicators.html.erb +29 -0
- data/app/components/alchemy/admin/page_status_indicators.rb +9 -0
- data/app/components/alchemy/admin/publish_element_button.html.erb +12 -4
- data/app/components/alchemy/ingredients/headline_editor.rb +1 -1
- data/app/controllers/alchemy/admin/dashboard/widgets_controller.rb +21 -0
- data/app/controllers/alchemy/admin/dashboard_controller.rb +3 -12
- data/app/controllers/alchemy/pages_controller.rb +5 -4
- data/app/helpers/alchemy/elements_block_helper.rb +1 -0
- data/app/javascript/alchemy_admin/components/auto_submit.js +15 -9
- data/app/javascript/alchemy_admin/components/char_counter.js +17 -7
- data/app/javascript/alchemy_admin/components/clipboard_button.js +2 -6
- data/app/javascript/alchemy_admin/components/color_select.js +13 -4
- data/app/javascript/alchemy_admin/components/datepicker.js +11 -14
- data/app/javascript/alchemy_admin/components/dialog_link.js +5 -2
- data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +6 -3
- data/app/javascript/alchemy_admin/components/element_editor.js +45 -28
- data/app/javascript/alchemy_admin/components/element_select.js +7 -4
- data/app/javascript/alchemy_admin/components/elements_window.js +38 -31
- data/app/javascript/alchemy_admin/components/elements_window_handle.js +7 -3
- data/app/javascript/alchemy_admin/components/file_editor.js +5 -2
- data/app/javascript/alchemy_admin/components/ingredient_group.js +6 -4
- data/app/javascript/alchemy_admin/components/link_buttons/link_button.js +1 -2
- data/app/javascript/alchemy_admin/components/link_buttons/unlink_button.js +1 -2
- data/app/javascript/alchemy_admin/components/link_buttons.js +6 -2
- data/app/javascript/alchemy_admin/components/list_filter.js +44 -29
- data/app/javascript/alchemy_admin/components/message.js +22 -15
- data/app/javascript/alchemy_admin/components/overlay.js +5 -7
- data/app/javascript/alchemy_admin/components/page_publication_fields.js +38 -25
- data/app/javascript/alchemy_admin/components/picture_description_select.js +5 -2
- data/app/javascript/alchemy_admin/components/picture_editor.js +5 -10
- data/app/javascript/alchemy_admin/components/picture_thumbnail.js +4 -5
- data/app/javascript/alchemy_admin/components/preview_window.js +5 -10
- data/app/javascript/alchemy_admin/components/publish_page_button.js +2 -5
- data/app/javascript/alchemy_admin/components/remote_select.js +53 -23
- data/app/javascript/alchemy_admin/components/select.js +169 -26
- data/app/javascript/alchemy_admin/components/sortable_elements.js +1 -1
- data/app/javascript/alchemy_admin/components/spinner.js +11 -11
- data/app/javascript/alchemy_admin/components/tags_autocomplete.js +9 -1
- data/app/javascript/alchemy_admin/components/tinymce.js +16 -22
- data/app/javascript/alchemy_admin/components/uploader/file_upload.js +48 -45
- data/app/javascript/alchemy_admin/components/uploader/progress.js +70 -84
- data/app/javascript/alchemy_admin/components/uploader.js +71 -46
- data/app/javascript/alchemy_admin/dialog.js +3 -0
- data/app/javascript/alchemy_admin/hotkeys.js +0 -18
- data/app/javascript/alchemy_admin/image_cropper.js +7 -9
- data/app/javascript/alchemy_admin/initializer.js +21 -0
- data/app/javascript/alchemy_admin/utils/dispatch_page_dirty_event.js +7 -0
- data/app/javascript/tinymce/plugins/alchemy_link/index.js +9 -0
- data/app/jobs/alchemy/base_job.rb +2 -2
- data/app/jobs/alchemy/invalidate_elements_cache_job.rb +33 -0
- data/app/models/alchemy/page/page_naming.rb +28 -5
- data/app/models/alchemy/page/page_natures.rb +7 -2
- data/app/models/alchemy/page/page_scopes.rb +2 -2
- data/app/models/alchemy/page/url_path.rb +7 -2
- data/app/models/alchemy/page.rb +2 -2
- data/app/models/alchemy/page_definition.rb +1 -0
- data/app/models/alchemy/permissions.rb +1 -1
- data/app/models/concerns/alchemy/relatable_resource.rb +8 -0
- data/app/services/alchemy/page_finder.rb +88 -0
- data/app/stylesheets/alchemy/_custom-properties.scss +6 -4
- data/app/stylesheets/alchemy/_mixins.scss +1 -7
- data/app/stylesheets/alchemy/_themes.scss +13 -1
- data/app/stylesheets/alchemy/admin/_tom-select.scss +240 -0
- data/app/stylesheets/alchemy/admin/archive.scss +0 -1
- data/app/stylesheets/alchemy/admin/base.scss +0 -19
- data/app/stylesheets/alchemy/admin/dashboard.scss +395 -28
- data/app/stylesheets/alchemy/admin/elements.scss +14 -17
- data/app/stylesheets/alchemy/admin/form_fields.scss +3 -3
- data/app/stylesheets/alchemy/admin/forms.scss +107 -93
- data/app/stylesheets/alchemy/admin/icons.scss +28 -0
- data/app/stylesheets/alchemy/admin/image_library.scss +20 -10
- data/app/stylesheets/alchemy/admin/navigation.scss +4 -1
- data/app/stylesheets/alchemy/admin/popover.scss +3 -5
- data/app/stylesheets/alchemy/admin/resource_info.scss +11 -17
- data/app/stylesheets/alchemy/admin/shoelace.scss +8 -0
- data/app/stylesheets/alchemy/admin/sitemap.scss +5 -0
- data/app/stylesheets/alchemy/admin/tables.scss +32 -3
- data/app/stylesheets/alchemy/admin/toolbar.scss +0 -1
- data/app/stylesheets/alchemy/admin.scss +1 -0
- data/app/stylesheets/tinymce/skins/ui/alchemy/skin.scss +0 -4
- data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +0 -4
- data/app/types/alchemy/wildcard_url_type.rb +48 -0
- data/app/views/alchemy/_menubar.html.erb +1 -5
- data/app/views/alchemy/admin/attachments/edit.html.erb +6 -3
- data/app/views/alchemy/admin/dashboard/_dashboard.html.erb +3 -2
- data/app/views/alchemy/admin/dashboard/_footer.html.erb +22 -0
- data/app/views/alchemy/admin/dashboard/_stats.html.erb +7 -0
- data/app/views/alchemy/admin/dashboard/_top.html.erb +4 -12
- data/app/views/alchemy/admin/dashboard/_widgets.html.erb +7 -0
- data/app/views/alchemy/admin/dashboard/index.html.erb +0 -17
- data/app/views/alchemy/admin/dashboard/info.html.erb +1 -62
- data/app/views/alchemy/admin/dashboard/widgets/show.html.erb +3 -0
- data/app/views/alchemy/admin/elements/_form.html.erb +2 -1
- data/app/views/alchemy/admin/elements/_schedule.html.erb +2 -15
- data/app/views/alchemy/admin/elements/_schedule_fields.html.erb +2 -0
- data/app/views/alchemy/admin/layoutpages/edit.html.erb +6 -3
- data/app/views/alchemy/admin/nodes/_page_nodes.html.erb +10 -8
- data/app/views/alchemy/admin/pages/_form.html.erb +25 -19
- data/app/views/alchemy/admin/pages/_publication_fields.html.erb +2 -32
- data/app/views/alchemy/admin/pages/_table.html.erb +1 -18
- data/app/views/alchemy/admin/pages/configure.html.erb +2 -2
- data/app/views/alchemy/admin/pages/info.html.erb +6 -0
- data/app/views/alchemy/admin/resources/_form.html.erb +7 -4
- data/app/views/alchemy/admin/resources/edit.html.erb +3 -1
- data/app/views/alchemy/admin/resources/new.html.erb +3 -1
- data/app/views/alchemy/admin/styleguide/index.html.erb +52 -30
- data/app/views/alchemy/admin/translations/_en.js +4 -0
- data/app/views/layouts/alchemy/admin.html.erb +3 -3
- data/config/importmap.rb +2 -0
- data/config/locales/alchemy.en.yml +15 -0
- data/config/routes.rb +1 -0
- data/lib/alchemy/configuration/class_option.rb +46 -3
- data/lib/alchemy/configuration/collection_option.rb +4 -0
- data/lib/alchemy/configurations/dashboard.rb +79 -0
- data/lib/alchemy/configurations/main.rb +15 -0
- data/lib/alchemy/engine.rb +9 -3
- data/lib/alchemy/sprockets/skip_builds_compression.rb +33 -0
- data/lib/alchemy/test_support/capybara_helpers.rb +17 -0
- data/lib/alchemy/test_support/relatable_resource_examples.rb +20 -0
- data/lib/alchemy/test_support/rspec_matchers.rb +8 -0
- data/lib/alchemy/test_support/shared_publishable_examples.rb +38 -31
- data/lib/alchemy/tinymce.rb +1 -1
- data/lib/alchemy/version.rb +17 -3
- data/vendor/javascript/cropperjs.min.js +1 -1
- data/vendor/javascript/flatpickr.min.js +1 -1
- data/vendor/javascript/floating-ui.min.js +1 -0
- data/vendor/javascript/keymaster.min.js +1 -1
- data/vendor/javascript/rails-ujs.min.js +1 -1
- data/vendor/javascript/shoelace.min.js +93 -93
- data/vendor/javascript/sortable.min.js +1 -1
- data/vendor/javascript/tinymce.min.js +5 -1
- data/vendor/javascript/tom-select.min.js +1 -0
- metadata +57 -18
- data/app/javascript/alchemy_admin/components/alchemy_html_element.js +0 -129
- data/app/views/alchemy/admin/dashboard/_left_column.html.erb +0 -4
- data/app/views/alchemy/admin/dashboard/_right_column.html.erb +0 -9
- data/app/views/alchemy/admin/dashboard/widgets/_locked_pages.html.erb +0 -52
- data/app/views/alchemy/admin/dashboard/widgets/_recent_pages.html.erb +0 -34
- data/app/views/alchemy/admin/dashboard/widgets/_sites.html.erb +0 -25
- data/app/views/alchemy/admin/dashboard/widgets/_users.html.erb +0 -21
- data/app/views/alchemy/admin/languages/edit.html.erb +0 -1
- data/app/views/alchemy/admin/languages/new.html.erb +0 -1
- data/app/views/alchemy/admin/sites/edit.html.erb +0 -1
- data/app/views/alchemy/admin/sites/new.html.erb +0 -1
|
@@ -1,38 +1,22 @@
|
|
|
1
|
-
import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
|
|
2
1
|
import { formatFileSize } from "alchemy_admin/utils/format"
|
|
3
2
|
import { translate } from "alchemy_admin/i18n"
|
|
4
3
|
import { growl } from "alchemy_admin/growler"
|
|
5
4
|
|
|
6
|
-
export class FileUpload extends
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* Initialize the component with file and request
|
|
22
|
-
* @param {File} file
|
|
23
|
-
* @param {XMLHttpRequest} request
|
|
24
|
-
*/
|
|
25
|
-
initialize(file, request) {
|
|
26
|
-
this.file = file
|
|
27
|
-
this.request = request
|
|
28
|
-
this.progressEventTotal = file ? file.size : 0
|
|
29
|
-
|
|
30
|
-
this._validateFile()
|
|
31
|
-
this._addRequestEventListener()
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
render() {
|
|
35
|
-
return `
|
|
5
|
+
export class FileUpload extends HTMLElement {
|
|
6
|
+
// public — used by callers (Uploader, Progress, tests)
|
|
7
|
+
file = null
|
|
8
|
+
request = null
|
|
9
|
+
progressEventLoaded = 0
|
|
10
|
+
progressEventTotal = 0
|
|
11
|
+
|
|
12
|
+
// private — backing state for getters/setters
|
|
13
|
+
#valid = true
|
|
14
|
+
#value = 0
|
|
15
|
+
#status = undefined
|
|
16
|
+
#errorMessage = ""
|
|
17
|
+
|
|
18
|
+
connectedCallback() {
|
|
19
|
+
this.innerHTML = `
|
|
36
20
|
<sl-progress-bar value="${this.value}"></sl-progress-bar>
|
|
37
21
|
<div class="description">
|
|
38
22
|
<span class="file-name">${this.file?.name}</span>
|
|
@@ -45,9 +29,7 @@ export class FileUpload extends AlchemyHTMLElement {
|
|
|
45
29
|
</button>
|
|
46
30
|
</sl-tooltip>
|
|
47
31
|
`
|
|
48
|
-
}
|
|
49
32
|
|
|
50
|
-
afterRender() {
|
|
51
33
|
this.querySelector("button").addEventListener("click", () => this.cancel())
|
|
52
34
|
|
|
53
35
|
if (this.file?.type.includes("image")) {
|
|
@@ -61,6 +43,21 @@ export class FileUpload extends AlchemyHTMLElement {
|
|
|
61
43
|
}
|
|
62
44
|
}
|
|
63
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Initialize the component with file and request
|
|
48
|
+
* @param {File} file
|
|
49
|
+
* @param {XMLHttpRequest} request
|
|
50
|
+
*/
|
|
51
|
+
initialize(file, request) {
|
|
52
|
+
this.file = file
|
|
53
|
+
this.request = request
|
|
54
|
+
this.progressEventTotal = file ? file.size : 0
|
|
55
|
+
this.status = "in-progress"
|
|
56
|
+
|
|
57
|
+
this.#validateFile()
|
|
58
|
+
this.#addRequestEventListener()
|
|
59
|
+
}
|
|
60
|
+
|
|
64
61
|
/**
|
|
65
62
|
* cancel the upload
|
|
66
63
|
*/
|
|
@@ -72,11 +69,18 @@ export class FileUpload extends AlchemyHTMLElement {
|
|
|
72
69
|
}
|
|
73
70
|
}
|
|
74
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Dispatches a custom event with given name, namespaced under `Alchemy.`.
|
|
74
|
+
* @param {string} name The name of the custom event
|
|
75
|
+
*/
|
|
76
|
+
dispatchCustomEvent(name) {
|
|
77
|
+
this.dispatchEvent(new CustomEvent(`Alchemy.${name}`, { bubbles: true }))
|
|
78
|
+
}
|
|
79
|
+
|
|
75
80
|
/**
|
|
76
81
|
* validate given file with the `Alchemy.uploader_defaults` - configuration
|
|
77
|
-
* @private
|
|
78
82
|
*/
|
|
79
|
-
|
|
83
|
+
#validateFile() {
|
|
80
84
|
const config = Alchemy.uploader_defaults
|
|
81
85
|
const maxFileSize = config.file_size_limit * Math.pow(1024, 2) // in Byte
|
|
82
86
|
let errorMessage = undefined
|
|
@@ -107,9 +111,8 @@ export class FileUpload extends AlchemyHTMLElement {
|
|
|
107
111
|
|
|
108
112
|
/**
|
|
109
113
|
* register event listeners to react on request changes
|
|
110
|
-
* @private
|
|
111
114
|
*/
|
|
112
|
-
|
|
115
|
+
#addRequestEventListener() {
|
|
113
116
|
// prevent errors if the component will be called without a request - object
|
|
114
117
|
if (!this.request) {
|
|
115
118
|
return
|
|
@@ -149,14 +152,14 @@ export class FileUpload extends AlchemyHTMLElement {
|
|
|
149
152
|
* @returns {string}
|
|
150
153
|
*/
|
|
151
154
|
get errorMessage() {
|
|
152
|
-
return this
|
|
155
|
+
return this.#errorMessage || ""
|
|
153
156
|
}
|
|
154
157
|
|
|
155
158
|
/**
|
|
156
159
|
* @param {string} message
|
|
157
160
|
*/
|
|
158
161
|
set errorMessage(message) {
|
|
159
|
-
this
|
|
162
|
+
this.#errorMessage = message
|
|
160
163
|
const errorMessageContainer = this.querySelector(".error-message")
|
|
161
164
|
if (errorMessageContainer) {
|
|
162
165
|
errorMessageContainer.textContent = message
|
|
@@ -215,14 +218,14 @@ export class FileUpload extends AlchemyHTMLElement {
|
|
|
215
218
|
* @returns {string}
|
|
216
219
|
*/
|
|
217
220
|
get status() {
|
|
218
|
-
return this
|
|
221
|
+
return this.#status
|
|
219
222
|
}
|
|
220
223
|
|
|
221
224
|
/**
|
|
222
225
|
* @param {string} status
|
|
223
226
|
*/
|
|
224
227
|
set status(status) {
|
|
225
|
-
this
|
|
228
|
+
this.#status = status
|
|
226
229
|
this.className = status
|
|
227
230
|
|
|
228
231
|
this.progressElement?.toggleAttribute(
|
|
@@ -235,14 +238,14 @@ export class FileUpload extends AlchemyHTMLElement {
|
|
|
235
238
|
* @returns {boolean}
|
|
236
239
|
*/
|
|
237
240
|
get valid() {
|
|
238
|
-
return this
|
|
241
|
+
return this.#valid
|
|
239
242
|
}
|
|
240
243
|
|
|
241
244
|
/**
|
|
242
245
|
* @param {boolean} isValid
|
|
243
246
|
*/
|
|
244
247
|
set valid(isValid) {
|
|
245
|
-
this
|
|
248
|
+
this.#valid = isValid
|
|
246
249
|
this.classList.toggle("invalid", !isValid)
|
|
247
250
|
}
|
|
248
251
|
|
|
@@ -251,14 +254,14 @@ export class FileUpload extends AlchemyHTMLElement {
|
|
|
251
254
|
* @returns {number}
|
|
252
255
|
*/
|
|
253
256
|
get value() {
|
|
254
|
-
return this
|
|
257
|
+
return this.#value
|
|
255
258
|
}
|
|
256
259
|
|
|
257
260
|
/**
|
|
258
261
|
* @param {number} value
|
|
259
262
|
*/
|
|
260
263
|
set value(value) {
|
|
261
|
-
this
|
|
264
|
+
this.#value = value
|
|
262
265
|
if (this.progressElement) {
|
|
263
266
|
this.progressElement.value = value
|
|
264
267
|
}
|
|
@@ -1,36 +1,41 @@
|
|
|
1
|
-
import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
|
|
2
1
|
import { FileUpload } from "alchemy_admin/components/uploader/file_upload"
|
|
3
2
|
import { formatFileSize } from "alchemy_admin/utils/format"
|
|
4
3
|
import { translate } from "alchemy_admin/i18n"
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
const template = (buttonLabel, fileCount) => `
|
|
6
|
+
<sl-progress-bar value="0"></sl-progress-bar>
|
|
7
|
+
<div class="overall-progress-value">
|
|
8
|
+
<span class="value-text"></span>
|
|
9
|
+
|
|
10
|
+
<sl-tooltip content="${buttonLabel}">
|
|
11
|
+
<button class="icon_button" aria-label="${buttonLabel}">
|
|
12
|
+
<alchemy-icon name="close"></alchemy-icon>
|
|
13
|
+
</button>
|
|
14
|
+
</sl-tooltip>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="single-uploads" style="--progress-columns: ${
|
|
17
|
+
fileCount > 3 ? 3 : fileCount
|
|
18
|
+
}"></div>
|
|
19
|
+
<div class="overall-upload-value value-text"></div>
|
|
20
|
+
`
|
|
21
|
+
|
|
22
|
+
export class Progress extends HTMLElement {
|
|
23
|
+
// public — accessed by Uploader and tests
|
|
24
|
+
fileCount = 0
|
|
25
|
+
|
|
26
|
+
// private — backing state and internals
|
|
27
|
+
#fileUploads = []
|
|
28
|
+
#buttonLabel = translate("Cancel all uploads")
|
|
29
|
+
#actionButton = null
|
|
7
30
|
#visible = false
|
|
31
|
+
#handleFileChange = () => this.#updateView()
|
|
8
32
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
this.buttonLabel = translate("Cancel all uploads")
|
|
12
|
-
this.fileUploads = []
|
|
13
|
-
this.fileCount = 0
|
|
14
|
-
this.className = "in-progress"
|
|
33
|
+
connectedCallback() {
|
|
34
|
+
this.innerHTML = template(this.#buttonLabel, this.fileCount)
|
|
15
35
|
this.visible = true
|
|
16
|
-
this.handleFileChange = () => this._updateView()
|
|
17
|
-
}
|
|
18
36
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* @param {FileUpload[]} fileUploads
|
|
22
|
-
*/
|
|
23
|
-
initialize(fileUploads = []) {
|
|
24
|
-
this.fileUploads = fileUploads
|
|
25
|
-
this.fileCount = fileUploads.length
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* append file progress - components for each file
|
|
30
|
-
*/
|
|
31
|
-
afterRender() {
|
|
32
|
-
this.actionButton = this.querySelector("button")
|
|
33
|
-
this.actionButton.addEventListener("click", () => {
|
|
37
|
+
this.#actionButton = this.querySelector("button")
|
|
38
|
+
this.#actionButton.addEventListener("click", () => {
|
|
34
39
|
if (this.finished) {
|
|
35
40
|
this.onComplete(this.status)
|
|
36
41
|
} else {
|
|
@@ -38,34 +43,38 @@ export class Progress extends AlchemyHTMLElement {
|
|
|
38
43
|
}
|
|
39
44
|
})
|
|
40
45
|
|
|
41
|
-
this
|
|
46
|
+
this.#fileUploads.forEach((fileUpload) => {
|
|
42
47
|
this.querySelector(".single-uploads").append(fileUpload)
|
|
43
48
|
})
|
|
49
|
+
|
|
50
|
+
this.#updateView()
|
|
51
|
+
this.addEventListener("Alchemy.FileUpload.Change", this.#handleFileChange)
|
|
44
52
|
}
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
upload.cancel()
|
|
52
|
-
})
|
|
53
|
-
this._setupCloseButton()
|
|
54
|
+
disconnectedCallback() {
|
|
55
|
+
this.removeEventListener(
|
|
56
|
+
"Alchemy.FileUpload.Change",
|
|
57
|
+
this.#handleFileChange
|
|
58
|
+
)
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
/**
|
|
57
|
-
*
|
|
62
|
+
* Initialize the component with file uploads
|
|
63
|
+
* @param {FileUpload[]} fileUploads
|
|
58
64
|
*/
|
|
59
|
-
|
|
60
|
-
this
|
|
61
|
-
this.
|
|
65
|
+
initialize(fileUploads = []) {
|
|
66
|
+
this.#fileUploads = fileUploads
|
|
67
|
+
this.fileCount = fileUploads.length
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
/**
|
|
65
|
-
*
|
|
71
|
+
* cancel requests in all remaining uploads
|
|
66
72
|
*/
|
|
67
|
-
|
|
68
|
-
this.
|
|
73
|
+
cancel() {
|
|
74
|
+
this.#activeUploads().forEach((upload) => {
|
|
75
|
+
upload.cancel()
|
|
76
|
+
})
|
|
77
|
+
this.#setupCloseButton()
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
/**
|
|
@@ -75,51 +84,29 @@ export class Progress extends AlchemyHTMLElement {
|
|
|
75
84
|
*/
|
|
76
85
|
onComplete(_status) {}
|
|
77
86
|
|
|
78
|
-
render() {
|
|
79
|
-
return `
|
|
80
|
-
<sl-progress-bar value="0"></sl-progress-bar>
|
|
81
|
-
<div class="overall-progress-value">
|
|
82
|
-
<span class="value-text"></span>
|
|
83
|
-
|
|
84
|
-
<sl-tooltip content="${this.buttonLabel}">
|
|
85
|
-
<button class="icon_button" aria-label="${this.buttonLabel}">
|
|
86
|
-
<alchemy-icon name="close"></alchemy-icon>
|
|
87
|
-
</button>
|
|
88
|
-
</sl-tooltip>
|
|
89
|
-
</div>
|
|
90
|
-
<div class="single-uploads" style="--progress-columns: ${
|
|
91
|
-
this.fileCount > 3 ? 3 : this.fileCount
|
|
92
|
-
}"></div>
|
|
93
|
-
<div class="overall-upload-value value-text"></div>
|
|
94
|
-
`
|
|
95
|
-
}
|
|
96
|
-
|
|
97
87
|
/**
|
|
98
88
|
* get all active upload components
|
|
99
89
|
* @returns {FileUpload[]}
|
|
100
|
-
* @private
|
|
101
90
|
*/
|
|
102
|
-
|
|
103
|
-
return this
|
|
91
|
+
#activeUploads() {
|
|
92
|
+
return this.#fileUploads.filter((upload) => upload.active)
|
|
104
93
|
}
|
|
105
94
|
|
|
106
95
|
/**
|
|
107
96
|
* replace cancel button to be the close button
|
|
108
|
-
* @private
|
|
109
97
|
*/
|
|
110
|
-
|
|
111
|
-
this
|
|
112
|
-
this
|
|
113
|
-
this
|
|
98
|
+
#setupCloseButton() {
|
|
99
|
+
this.#buttonLabel = translate("Close")
|
|
100
|
+
this.#actionButton.ariaLabel = this.#buttonLabel
|
|
101
|
+
this.#actionButton.parentElement.content = this.#buttonLabel // update tooltip content
|
|
114
102
|
}
|
|
115
103
|
|
|
116
104
|
/**
|
|
117
105
|
* @param {string} field
|
|
118
106
|
* @returns {number}
|
|
119
|
-
* @private
|
|
120
107
|
*/
|
|
121
|
-
|
|
122
|
-
return this
|
|
108
|
+
#sumFileProgresses(field) {
|
|
109
|
+
return this.#activeUploads().reduce(
|
|
123
110
|
(accumulator, upload) => upload[field] + accumulator,
|
|
124
111
|
0
|
|
125
112
|
)
|
|
@@ -127,9 +114,8 @@ export class Progress extends AlchemyHTMLElement {
|
|
|
127
114
|
|
|
128
115
|
/**
|
|
129
116
|
* don't render the whole element new, because it would prevent selecting buttons
|
|
130
|
-
* @private
|
|
131
117
|
*/
|
|
132
|
-
|
|
118
|
+
#updateView() {
|
|
133
119
|
const status = this.status
|
|
134
120
|
this.className = status
|
|
135
121
|
|
|
@@ -147,7 +133,7 @@ export class Progress extends AlchemyHTMLElement {
|
|
|
147
133
|
this.overallUploadSize
|
|
148
134
|
|
|
149
135
|
if (this.finished) {
|
|
150
|
-
this
|
|
136
|
+
this.#setupCloseButton()
|
|
151
137
|
this.onComplete(status)
|
|
152
138
|
} else {
|
|
153
139
|
this.visible = true
|
|
@@ -158,34 +144,34 @@ export class Progress extends AlchemyHTMLElement {
|
|
|
158
144
|
* @returns {boolean}
|
|
159
145
|
*/
|
|
160
146
|
get finished() {
|
|
161
|
-
return this
|
|
147
|
+
return this.#activeUploads().every((entry) => entry.finished)
|
|
162
148
|
}
|
|
163
149
|
|
|
164
150
|
/**
|
|
165
151
|
* @returns {string}
|
|
166
152
|
*/
|
|
167
153
|
get overallUploadSize() {
|
|
168
|
-
const uploadedFileCount = this
|
|
154
|
+
const uploadedFileCount = this.#activeUploads().filter(
|
|
169
155
|
(fileProgress) => fileProgress.value >= 100
|
|
170
156
|
).length
|
|
171
157
|
const overallProgressValue = `${
|
|
172
158
|
this.totalProgress
|
|
173
|
-
}% (${uploadedFileCount} / ${this
|
|
159
|
+
}% (${uploadedFileCount} / ${this.#activeUploads().length})`
|
|
174
160
|
|
|
175
161
|
return `${formatFileSize(
|
|
176
|
-
this
|
|
177
|
-
)} / ${formatFileSize(this
|
|
162
|
+
this.#sumFileProgresses("progressEventLoaded")
|
|
163
|
+
)} / ${formatFileSize(this.#sumFileProgresses("progressEventTotal"))}`
|
|
178
164
|
}
|
|
179
165
|
|
|
180
166
|
/**
|
|
181
167
|
* @returns {string}
|
|
182
168
|
*/
|
|
183
169
|
get overallProgressValue() {
|
|
184
|
-
const uploadedFileCount = this
|
|
170
|
+
const uploadedFileCount = this.#activeUploads().filter(
|
|
185
171
|
(fileProgress) => fileProgress.value >= 100
|
|
186
172
|
).length
|
|
187
173
|
return `${this.totalProgress}% (${uploadedFileCount} / ${
|
|
188
|
-
this
|
|
174
|
+
this.#activeUploads().length
|
|
189
175
|
})`
|
|
190
176
|
}
|
|
191
177
|
|
|
@@ -201,7 +187,7 @@ export class Progress extends AlchemyHTMLElement {
|
|
|
201
187
|
* @returns {string}
|
|
202
188
|
*/
|
|
203
189
|
get status() {
|
|
204
|
-
const uploadsStatuses = this
|
|
190
|
+
const uploadsStatuses = this.#activeUploads().map(
|
|
205
191
|
(upload) => upload.className
|
|
206
192
|
)
|
|
207
193
|
|
|
@@ -227,12 +213,12 @@ export class Progress extends AlchemyHTMLElement {
|
|
|
227
213
|
* @returns {number}
|
|
228
214
|
*/
|
|
229
215
|
get totalProgress() {
|
|
230
|
-
const totalSize = this
|
|
216
|
+
const totalSize = this.#activeUploads().reduce(
|
|
231
217
|
(accumulator, upload) => accumulator + upload.file.size,
|
|
232
218
|
0
|
|
233
219
|
)
|
|
234
220
|
let totalProgress = Math.ceil(
|
|
235
|
-
this
|
|
221
|
+
this.#activeUploads().reduce((accumulator, upload) => {
|
|
236
222
|
const weight = upload.file.size / totalSize
|
|
237
223
|
return upload.value * weight + accumulator
|
|
238
224
|
}, 0)
|
|
@@ -3,36 +3,75 @@
|
|
|
3
3
|
* @property {string} name
|
|
4
4
|
* @property {number} size
|
|
5
5
|
*/
|
|
6
|
-
import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
|
|
7
6
|
import { Progress } from "alchemy_admin/components/uploader/progress"
|
|
8
7
|
import { FileUpload } from "alchemy_admin/components/uploader/file_upload"
|
|
9
8
|
import { translate } from "alchemy_admin/i18n"
|
|
10
9
|
import { getToken } from "alchemy_admin/utils/ajax"
|
|
11
10
|
|
|
12
|
-
export class Uploader extends
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
11
|
+
export class Uploader extends HTMLElement {
|
|
12
|
+
#dropzoneElement = null
|
|
13
|
+
#isDraggedOver = false
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
this.fileInput.addEventListener("change",
|
|
19
|
-
this._uploadFiles(Array.from(event.target.files))
|
|
20
|
-
})
|
|
15
|
+
connectedCallback() {
|
|
16
|
+
this.fileInput.addEventListener("change", this.#onFileInputChange)
|
|
21
17
|
if (this.dropzone) {
|
|
22
|
-
this
|
|
18
|
+
this.#setupDropZone()
|
|
23
19
|
}
|
|
24
20
|
this.addEventListener("Alchemy.upload.successful", this)
|
|
25
21
|
}
|
|
26
22
|
|
|
23
|
+
disconnectedCallback() {
|
|
24
|
+
this.fileInput?.removeEventListener("change", this.#onFileInputChange)
|
|
25
|
+
if (this.#dropzoneElement) {
|
|
26
|
+
this.#dropzoneElement.removeEventListener(
|
|
27
|
+
"dragleave",
|
|
28
|
+
this.#onDropzoneDragleave
|
|
29
|
+
)
|
|
30
|
+
this.#dropzoneElement.removeEventListener("drop", this.#onDropzoneDrop)
|
|
31
|
+
this.#dropzoneElement.removeEventListener(
|
|
32
|
+
"dragover",
|
|
33
|
+
this.#onDropzoneDragover
|
|
34
|
+
)
|
|
35
|
+
this.#dropzoneElement = null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
handleEvent(evt) {
|
|
28
40
|
switch (evt.type) {
|
|
29
41
|
case "Alchemy.upload.successful":
|
|
30
|
-
this
|
|
42
|
+
this.#handleUploadComplete()
|
|
31
43
|
break
|
|
32
44
|
}
|
|
33
45
|
}
|
|
34
46
|
|
|
35
|
-
|
|
47
|
+
#onFileInputChange = (event) => {
|
|
48
|
+
this.uploadFiles(Array.from(event.target.files))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#toggleDropzoneClass = (enabled) => {
|
|
52
|
+
if (this.#isDraggedOver !== enabled) {
|
|
53
|
+
this.#isDraggedOver = enabled
|
|
54
|
+
this.#dropzoneElement.classList.toggle("dragover")
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#onDropzoneDragleave = () => this.#toggleDropzoneClass(false)
|
|
59
|
+
|
|
60
|
+
#onDropzoneDrop = async (event) => {
|
|
61
|
+
event.preventDefault()
|
|
62
|
+
this.#toggleDropzoneClass(false)
|
|
63
|
+
|
|
64
|
+
const files = [...event.dataTransfer.items].map((item) => item.getAsFile())
|
|
65
|
+
|
|
66
|
+
this.uploadFiles(files)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#onDropzoneDragover = (event) => {
|
|
70
|
+
event.preventDefault() // dragover has to be disabled to use the custom drop event
|
|
71
|
+
this.#toggleDropzoneClass(true)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#handleUploadComplete() {
|
|
36
75
|
setTimeout(() => {
|
|
37
76
|
const url = this.redirectUrl
|
|
38
77
|
const turboFrame = this.closest("turbo-frame")
|
|
@@ -53,42 +92,22 @@ export class Uploader extends AlchemyHTMLElement {
|
|
|
53
92
|
* add dragover class to indicate, if the file is draggable
|
|
54
93
|
* @private
|
|
55
94
|
*/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const toggleDropzoneClass = (enabled) => {
|
|
61
|
-
if (isDraggedOver !== enabled) {
|
|
62
|
-
isDraggedOver = enabled
|
|
63
|
-
dropzoneElement.classList.toggle("dragover")
|
|
64
|
-
}
|
|
65
|
-
}
|
|
95
|
+
#setupDropZone() {
|
|
96
|
+
this.#dropzoneElement = document.querySelector(this.dropzone)
|
|
97
|
+
if (!this.#dropzoneElement) return
|
|
66
98
|
|
|
67
|
-
dropzoneElement.addEventListener(
|
|
68
|
-
|
|
99
|
+
this.#dropzoneElement.addEventListener(
|
|
100
|
+
"dragleave",
|
|
101
|
+
this.#onDropzoneDragleave
|
|
69
102
|
)
|
|
70
|
-
dropzoneElement.addEventListener("drop",
|
|
71
|
-
|
|
72
|
-
toggleDropzoneClass(false)
|
|
73
|
-
|
|
74
|
-
const files = [...event.dataTransfer.items].map((item) =>
|
|
75
|
-
item.getAsFile()
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
this._uploadFiles(files)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
dropzoneElement.addEventListener("dragover", (event) => {
|
|
82
|
-
event.preventDefault() // dragover has to be disabled to use the custom drop event
|
|
83
|
-
toggleDropzoneClass(true)
|
|
84
|
-
})
|
|
103
|
+
this.#dropzoneElement.addEventListener("drop", this.#onDropzoneDrop)
|
|
104
|
+
this.#dropzoneElement.addEventListener("dragover", this.#onDropzoneDragover)
|
|
85
105
|
}
|
|
86
106
|
|
|
87
107
|
/**
|
|
88
108
|
* @param {File[]} files
|
|
89
|
-
* @private
|
|
90
109
|
*/
|
|
91
|
-
|
|
110
|
+
uploadFiles(files) {
|
|
92
111
|
// prepare file progress bars and server request
|
|
93
112
|
let fileUploadCount = 0
|
|
94
113
|
|
|
@@ -102,13 +121,13 @@ export class Uploader extends AlchemyHTMLElement {
|
|
|
102
121
|
fileUpload.errorMessage = translate("Maximum number of files exceeded")
|
|
103
122
|
} else if (fileUpload.valid) {
|
|
104
123
|
fileUploadCount++
|
|
105
|
-
this
|
|
124
|
+
this.#submitFile(request, file)
|
|
106
125
|
}
|
|
107
126
|
|
|
108
127
|
return fileUpload
|
|
109
128
|
})
|
|
110
129
|
|
|
111
|
-
this
|
|
130
|
+
this.#createProgress(fileUploads)
|
|
112
131
|
}
|
|
113
132
|
|
|
114
133
|
/**
|
|
@@ -116,7 +135,7 @@ export class Uploader extends AlchemyHTMLElement {
|
|
|
116
135
|
* @param {File} file
|
|
117
136
|
* @private
|
|
118
137
|
*/
|
|
119
|
-
|
|
138
|
+
#submitFile(request, file) {
|
|
120
139
|
const form = this.querySelector("form")
|
|
121
140
|
const formData = new FormData(form)
|
|
122
141
|
formData.set(this.fileInput.name, file)
|
|
@@ -132,7 +151,7 @@ export class Uploader extends AlchemyHTMLElement {
|
|
|
132
151
|
* @param {FileUpload[]} fileUploads
|
|
133
152
|
* @private
|
|
134
153
|
*/
|
|
135
|
-
|
|
154
|
+
#createProgress(fileUploads) {
|
|
136
155
|
if (this.uploadProgress) {
|
|
137
156
|
this.uploadProgress.cancel()
|
|
138
157
|
document.body.removeChild(this.uploadProgress)
|
|
@@ -140,12 +159,18 @@ export class Uploader extends AlchemyHTMLElement {
|
|
|
140
159
|
this.uploadProgress = new Progress()
|
|
141
160
|
this.uploadProgress.initialize(fileUploads)
|
|
142
161
|
this.uploadProgress.onComplete = (status) => {
|
|
143
|
-
this.
|
|
162
|
+
this.dispatchEvent(
|
|
163
|
+
new CustomEvent(`Alchemy.upload.${status}`, { bubbles: true })
|
|
164
|
+
)
|
|
144
165
|
}
|
|
145
166
|
|
|
146
167
|
document.body.append(this.uploadProgress)
|
|
147
168
|
}
|
|
148
169
|
|
|
170
|
+
get dropzone() {
|
|
171
|
+
return this.getAttribute("dropzone")
|
|
172
|
+
}
|
|
173
|
+
|
|
149
174
|
/**
|
|
150
175
|
* @returns {HTMLInputElement}
|
|
151
176
|
*/
|
|
@@ -126,6 +126,9 @@ export class Dialog {
|
|
|
126
126
|
init() {
|
|
127
127
|
Hotkeys(this.dialog_body)
|
|
128
128
|
this.watch_remote_forms()
|
|
129
|
+
window.requestAnimationFrame(() => {
|
|
130
|
+
this.dialog_body.find("[autofocus]").focus()
|
|
131
|
+
})
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
// Watches ajax requests inside of dialog body and replaces the content accordingly
|
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
import "keymaster"
|
|
2
|
-
import { openDialog } from "alchemy_admin/dialog"
|
|
3
2
|
|
|
4
3
|
const bindedHotkeys = []
|
|
5
4
|
|
|
6
|
-
function showHelp(evt) {
|
|
7
|
-
if (
|
|
8
|
-
!$(evt.target).is("input, textarea") &&
|
|
9
|
-
String.fromCharCode(evt.which) === "?"
|
|
10
|
-
) {
|
|
11
|
-
openDialog("/admin/help", {
|
|
12
|
-
title: Alchemy.t("help"),
|
|
13
|
-
size: "400x492"
|
|
14
|
-
})
|
|
15
|
-
return false
|
|
16
|
-
} else {
|
|
17
|
-
return true
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
5
|
export default function (scope = document) {
|
|
22
6
|
// The scope can be a jQuery object because we still use jQuery in alchemy_admin/dialog.js.
|
|
23
7
|
if (scope instanceof jQuery) {
|
|
@@ -26,8 +10,6 @@ export default function (scope = document) {
|
|
|
26
10
|
|
|
27
11
|
// Unbind all previously registered hotkeys if we are not inside a dialog.
|
|
28
12
|
if (scope === document) {
|
|
29
|
-
document.removeEventListener("keypress", showHelp)
|
|
30
|
-
document.addEventListener("keypress", showHelp)
|
|
31
13
|
bindedHotkeys.forEach((hotkey) => key.unbind(hotkey))
|
|
32
14
|
}
|
|
33
15
|
|