alchemy_cms 7.3.4 → 7.4.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/CHANGELOG.md +67 -1
- data/Gemfile +10 -3
- data/README.md +2 -2
- data/Rakefile +2 -0
- data/alchemy_cms.gemspec +1 -4
- data/app/assets/builds/alchemy/admin.css +9 -1
- data/app/assets/builds/alchemy/admin.css.map +1 -1
- data/app/assets/builds/alchemy/custom-properties.css +1 -1
- data/app/assets/builds/alchemy/custom-properties.css.map +1 -1
- data/app/assets/builds/alchemy/preview.min.js +1 -0
- data/app/assets/builds/alchemy/welcome.css +1 -1
- data/app/assets/builds/alchemy/welcome.css.map +1 -1
- data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
- data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css.map +1 -1
- data/app/assets/config/alchemy_manifest.js +0 -4
- data/app/assets/javascripts/alchemy/admin.js +8 -6
- data/app/assets/stylesheets/alchemy/admin/elements.scss +43 -7
- data/app/assets/stylesheets/alchemy/admin/forms.scss +4 -0
- data/app/assets/stylesheets/alchemy/admin/image_library.scss +40 -26
- data/app/assets/stylesheets/alchemy/admin/navigation.scss +9 -1
- data/app/assets/stylesheets/alchemy/admin/preview_window.scss +22 -17
- data/app/assets/stylesheets/alchemy/admin.scss +1 -1
- data/app/assets/stylesheets/alchemy/custom-properties.css +2 -1
- data/app/components/alchemy/ingredients/link_view.rb +7 -1
- data/app/components/alchemy/ingredients/picture_view.rb +5 -2
- data/app/components/alchemy/ingredients/text_view.rb +4 -1
- data/app/components/concerns/alchemy/ingredients/link_target.rb +18 -0
- data/app/controllers/alchemy/admin/base_controller.rb +34 -5
- data/app/controllers/alchemy/admin/elements_controller.rb +2 -2
- data/app/controllers/alchemy/admin/languages_controller.rb +1 -1
- data/app/controllers/alchemy/admin/layoutpages_controller.rb +1 -0
- data/app/controllers/alchemy/admin/pages_controller.rb +6 -6
- data/app/controllers/alchemy/admin/resources_controller.rb +1 -1
- data/app/controllers/alchemy/elements_controller.rb +3 -0
- data/app/helpers/alchemy/admin/form_helper.rb +1 -1
- data/app/helpers/alchemy/admin/navigation_helper.rb +22 -1
- data/app/javascript/alchemy_admin/components/action.js +2 -1
- data/app/javascript/alchemy_admin/components/dialog_link.js +3 -18
- data/app/javascript/alchemy_admin/components/element_editor.js +9 -0
- data/app/javascript/alchemy_admin/components/elements_window.js +34 -0
- data/app/javascript/alchemy_admin/components/elements_window_handle.js +65 -0
- data/app/javascript/alchemy_admin/components/icon.js +2 -2
- data/app/javascript/alchemy_admin/components/index.js +1 -0
- data/app/javascript/alchemy_admin/components/preview_window.js +5 -5
- data/app/javascript/alchemy_admin/components/uploader/file_upload.js +1 -1
- data/app/javascript/alchemy_admin/confirm_dialog.js +9 -11
- data/app/javascript/alchemy_admin/dialog.js +329 -0
- data/app/javascript/alchemy_admin/hotkeys.js +3 -2
- data/app/javascript/alchemy_admin/image_cropper.js +57 -40
- data/app/javascript/alchemy_admin/image_overlay.js +73 -0
- data/app/javascript/alchemy_admin/initializer.js +51 -2
- data/app/javascript/alchemy_admin/link_dialog.js +2 -1
- data/app/javascript/alchemy_admin/node_tree.js +3 -1
- data/app/javascript/alchemy_admin/page_sorter.js +1 -1
- data/app/javascript/alchemy_admin/picture_selector.js +2 -1
- data/app/javascript/alchemy_admin/shoelace_theme.js +2 -2
- data/app/javascript/alchemy_admin/templates/compiled.js +1 -0
- data/app/javascript/alchemy_admin.js +10 -6
- data/app/javascript/preview.js +117 -0
- data/app/models/alchemy/image_cropper_settings.rb +3 -4
- data/app/views/alchemy/_menubar.html.erb +1 -1
- data/app/views/alchemy/_preview_mode_code.html.erb +1 -1
- data/app/views/alchemy/admin/crop.html.erb +19 -16
- data/app/views/alchemy/admin/dashboard/info.html.erb +1 -1
- data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +9 -8
- data/app/views/alchemy/admin/elements/_clipboard_button.html.erb +14 -0
- data/app/views/alchemy/admin/elements/_element.html.erb +2 -0
- data/app/views/alchemy/admin/elements/_form.html.erb +15 -13
- data/app/views/alchemy/admin/elements/create.turbo_stream.erb +34 -0
- data/app/views/alchemy/admin/elements/index.html.erb +3 -15
- data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +1 -1
- data/app/views/alchemy/admin/layoutpages/edit.html.erb +7 -5
- data/app/views/alchemy/admin/nodes/_form.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_current_page.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_form.html.erb +43 -40
- data/app/views/alchemy/admin/pages/_locked_page.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_page_layout_filter.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_sitemap.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_table.html.erb +2 -2
- data/app/views/alchemy/admin/pages/edit.html.erb +1 -1
- data/app/views/alchemy/admin/pages/update.turbo_stream.erb +39 -0
- data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +3 -4
- data/app/views/alchemy/admin/pictures/_picture_description_field.html.erb +7 -5
- data/app/views/alchemy/admin/pictures/index.html.erb +13 -9
- data/app/views/alchemy/admin/resources/_filter_bar.html.erb +1 -1
- data/app/views/layouts/alchemy/admin.html.erb +8 -4
- data/bun.lockb +0 -0
- data/bundles/tinymce.js +2 -0
- data/config/alchemy/config.yml +3 -3
- data/config/alchemy/modules.yml +7 -6
- data/config/importmap.rb +4 -0
- data/config/routes.rb +1 -1
- data/lib/alchemy/engine.rb +6 -0
- data/lib/alchemy/modules.rb +0 -27
- data/lib/alchemy/resource.rb +14 -4
- data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +10 -10
- data/lib/alchemy/tinymce.rb +2 -1
- data/lib/alchemy/upgrader/seven_point_four.rb +26 -0
- data/lib/alchemy/version.rb +1 -1
- data/lib/alchemy.rb +14 -0
- data/lib/alchemy_cms.rb +0 -2
- data/lib/generators/alchemy/ingredient/ingredient_generator.rb +5 -0
- data/lib/generators/alchemy/ingredient/templates/view.html.erb +1 -1
- data/lib/generators/alchemy/ingredient/templates/view_component.rb.tt +10 -0
- data/lib/generators/alchemy/install/install_generator.rb +0 -1
- data/lib/generators/alchemy/install/templates/elements.yml.tt +1 -1
- data/lib/tasks/alchemy/upgrade.rake +19 -20
- data/rollup.config.mjs +44 -1
- data/vendor/javascript/cropperjs.min.js +10 -0
- data/vendor/javascript/handlebars.min.js +29 -0
- data/vendor/javascript/jquery.min.js +2 -0
- data/vendor/javascript/select2.min.js +23 -0
- data/vendor/javascript/tinymce.min.js +1 -1
- metadata +40 -94
- data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +0 -271
- data/app/assets/javascripts/alchemy/alchemy.image_overlay.coffee +0 -54
- data/app/assets/javascripts/alchemy/alchemy.preview.js.coffee +0 -97
- data/app/assets/javascripts/alchemy/preview.js +0 -1
- data/app/assets/javascripts/alchemy/templates/index.js +0 -2
- data/app/javascript/alchemy_admin/gui.js +0 -12
- data/app/views/alchemy/admin/elements/create.js.erb +0 -35
- data/app/views/alchemy/admin/pages/update.js.erb +0 -43
- data/lib/alchemy/upgrader/seven_point_zero.rb +0 -36
- data/vendor/assets/images/Jcrop.gif +0 -0
- data/vendor/assets/javascripts/jquery_plugins/jquery.Jcrop.min.js +0 -7
- data/vendor/assets/javascripts/jquery_plugins/select2.js +0 -3729
- data/vendor/assets/stylesheets/jquery.Jcrop.min.css +0 -2
- data/vendor/assets/stylesheets/tinymce/skins/content/default/content.min.css +0 -1
- /data/app/{assets/javascripts/alchemy → javascript/alchemy_admin}/templates/node_folder.hbs +0 -0
- /data/app/{assets/javascripts/alchemy → javascript/alchemy_admin}/templates/page_folder.hbs +0 -0
- /data/app/{assets/javascripts/tinymce/icons/remixicons/icons.js → javascript/tinymce/icons/remixicons/index.js} +0 -0
- /data/app/{assets/javascripts/tinymce/plugins/alchemy_link/plugin.min.js → javascript/tinymce/plugins/alchemy_link/index.js} +0 -0
- /data/vendor/assets/{fonts → images}/remixicon.symbol.svg +0 -0
|
@@ -11,6 +11,7 @@ module Alchemy
|
|
|
11
11
|
# The Hash representing a Alchemy module
|
|
12
12
|
#
|
|
13
13
|
def alchemy_main_navigation_entry(alchemy_module)
|
|
14
|
+
validate_controller_existence!(alchemy_module)
|
|
14
15
|
render(
|
|
15
16
|
"alchemy/admin/partials/main_navigation_entry",
|
|
16
17
|
alchemy_module: alchemy_module,
|
|
@@ -126,7 +127,7 @@ module Alchemy
|
|
|
126
127
|
#
|
|
127
128
|
def route_from_engine_or_main_app(engine_name, url_options)
|
|
128
129
|
if engine_name.present?
|
|
129
|
-
|
|
130
|
+
send(engine_name).url_for(url_options)
|
|
130
131
|
else
|
|
131
132
|
main_app.url_for(url_options)
|
|
132
133
|
end
|
|
@@ -141,6 +142,26 @@ module Alchemy
|
|
|
141
142
|
url_options_for_navigation_entry(alchemy_module["navigation"] || {})
|
|
142
143
|
end
|
|
143
144
|
|
|
145
|
+
# Validates the existence of a given controller configuration.
|
|
146
|
+
#
|
|
147
|
+
# @param String
|
|
148
|
+
# The controller name
|
|
149
|
+
def validate_controller_existence!(definition_hash)
|
|
150
|
+
controllers = [definition_hash["navigation"]["controller"]]
|
|
151
|
+
|
|
152
|
+
if definition_hash["navigation"]["sub_navigation"].is_a?(Array)
|
|
153
|
+
controllers.concat(definition_hash["navigation"]["sub_navigation"].map { |x| x["controller"] })
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
controllers.each do |controller|
|
|
157
|
+
controller_const_name = "#{controller.camelize}Controller"
|
|
158
|
+
controller_const_name.constantize
|
|
159
|
+
rescue NameError
|
|
160
|
+
raise "Error in AlchemyCMS module definition: '#{definition_hash["name"]}'. Could not find the " \
|
|
161
|
+
"matching controller class #{controller_const_name} for the specified controller: '#{controller}'"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
144
165
|
# Returns a url options hash for given navigation entry.
|
|
145
166
|
#
|
|
146
167
|
# @param [Hash]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { reloadPreview } from "alchemy_admin/components/preview_window"
|
|
2
|
+
import { closeCurrentDialog } from "alchemy_admin/dialog"
|
|
2
3
|
import IngredientAnchorLink from "alchemy_admin/ingredient_anchor_link"
|
|
3
4
|
|
|
4
5
|
class Action extends HTMLElement {
|
|
@@ -10,7 +11,7 @@ class Action extends HTMLElement {
|
|
|
10
11
|
// add a intermediate closeCurrentDialog - action
|
|
11
12
|
// this will be gone, if all dialogs are working with a promise and
|
|
12
13
|
// we don't have to implicitly close the dialog
|
|
13
|
-
closeCurrentDialog
|
|
14
|
+
closeCurrentDialog,
|
|
14
15
|
reloadPreview,
|
|
15
16
|
updateAnchorIcon: IngredientAnchorLink.updateIcon
|
|
16
17
|
}
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
header_height: 36,
|
|
3
|
-
size: "400x300",
|
|
4
|
-
padding: true,
|
|
5
|
-
title: "",
|
|
6
|
-
modal: true,
|
|
7
|
-
overflow: "visible",
|
|
8
|
-
ready: () => {},
|
|
9
|
-
closed: () => {}
|
|
10
|
-
}
|
|
1
|
+
import { Dialog } from "alchemy_admin/dialog"
|
|
11
2
|
|
|
12
3
|
export class DialogLink extends HTMLAnchorElement {
|
|
13
4
|
constructor() {
|
|
@@ -23,10 +14,7 @@ export class DialogLink extends HTMLAnchorElement {
|
|
|
23
14
|
}
|
|
24
15
|
|
|
25
16
|
openDialog() {
|
|
26
|
-
this.dialog = new
|
|
27
|
-
this.getAttribute("href"),
|
|
28
|
-
this.dialogOptions
|
|
29
|
-
)
|
|
17
|
+
this.dialog = new Dialog(this.getAttribute("href"), this.dialogOptions)
|
|
30
18
|
this.dialog.open()
|
|
31
19
|
}
|
|
32
20
|
|
|
@@ -34,10 +22,7 @@ export class DialogLink extends HTMLAnchorElement {
|
|
|
34
22
|
const options = this.dataset.dialogOptions
|
|
35
23
|
? JSON.parse(this.dataset.dialogOptions)
|
|
36
24
|
: {}
|
|
37
|
-
return
|
|
38
|
-
...DEFAULTS,
|
|
39
|
-
...options
|
|
40
|
-
}
|
|
25
|
+
return options
|
|
41
26
|
}
|
|
42
27
|
|
|
43
28
|
get disabled() {
|
|
@@ -40,6 +40,15 @@ export class ElementEditor extends HTMLElement {
|
|
|
40
40
|
return
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// When newly created, focus the element and refresh the preview
|
|
44
|
+
if (this.hasAttribute("created")) {
|
|
45
|
+
this.focusElement()
|
|
46
|
+
this.previewWindow?.refresh().then(() => {
|
|
47
|
+
this.focusElementPreview()
|
|
48
|
+
})
|
|
49
|
+
this.removeAttribute("created")
|
|
50
|
+
}
|
|
51
|
+
|
|
43
52
|
// Init GUI elements
|
|
44
53
|
ImageLoader.init(this)
|
|
45
54
|
fileEditors(
|
|
@@ -2,6 +2,7 @@ import SortableElements from "alchemy_admin/sortable_elements"
|
|
|
2
2
|
|
|
3
3
|
class ElementsWindow extends HTMLElement {
|
|
4
4
|
#visible = true
|
|
5
|
+
#turboFrame = null
|
|
5
6
|
|
|
6
7
|
constructor() {
|
|
7
8
|
super()
|
|
@@ -19,6 +20,7 @@ class ElementsWindow extends HTMLElement {
|
|
|
19
20
|
?.trigger("FocusElementEditor.Alchemy")
|
|
20
21
|
}
|
|
21
22
|
SortableElements()
|
|
23
|
+
this.resize()
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
collapseAllElements() {
|
|
@@ -38,10 +40,12 @@ class ElementsWindow extends HTMLElement {
|
|
|
38
40
|
this.toggleButton
|
|
39
41
|
.querySelector("alchemy-icon")
|
|
40
42
|
.setAttribute("name", "menu-unfold")
|
|
43
|
+
this.resize()
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
hide() {
|
|
44
47
|
document.body.classList.remove("elements-window-visible")
|
|
48
|
+
document.body.style.removeProperty("--elements-window-width")
|
|
45
49
|
this.#visible = false
|
|
46
50
|
this.toggleButton.closest("sl-tooltip").content = Alchemy.t("Show elements")
|
|
47
51
|
this.toggleButton
|
|
@@ -49,6 +53,17 @@ class ElementsWindow extends HTMLElement {
|
|
|
49
53
|
.setAttribute("name", "menu-fold")
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
resize(width) {
|
|
57
|
+
if (width === undefined) {
|
|
58
|
+
width = this.widthFromCookie
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (width) {
|
|
62
|
+
document.body.style.setProperty("--elements-window-width", `${width}px`)
|
|
63
|
+
document.cookie = `alchemy-elements-window-width=${width}; SameSite=Lax; Path=/;`
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
get collapseButton() {
|
|
53
68
|
return this.querySelector("#collapse-all-elements-button")
|
|
54
69
|
}
|
|
@@ -61,6 +76,25 @@ class ElementsWindow extends HTMLElement {
|
|
|
61
76
|
return document.getElementById("alchemy_preview_window")
|
|
62
77
|
}
|
|
63
78
|
|
|
79
|
+
get turboFrame() {
|
|
80
|
+
if (!this.#turboFrame) {
|
|
81
|
+
this.#turboFrame = this.closest("turbo-frame")
|
|
82
|
+
}
|
|
83
|
+
return this.#turboFrame
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get widthFromCookie() {
|
|
87
|
+
return document.cookie
|
|
88
|
+
.split("; ")
|
|
89
|
+
.find((row) => row.startsWith("alchemy-elements-window-width="))
|
|
90
|
+
?.split("=")[1]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
set isDragged(dragged) {
|
|
94
|
+
this.turboFrame.style.transitionProperty = dragged ? "none" : null
|
|
95
|
+
this.turboFrame.style.pointerEvents = dragged ? "none" : null
|
|
96
|
+
}
|
|
97
|
+
|
|
64
98
|
#attachEvents() {
|
|
65
99
|
this.collapseButton?.addEventListener("click", () => {
|
|
66
100
|
this.collapseAllElements()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
class ElementsWindowHandle extends HTMLElement {
|
|
2
|
+
#dragging = false
|
|
3
|
+
#elementsWindow = null
|
|
4
|
+
#previewWindow = null
|
|
5
|
+
|
|
6
|
+
constructor() {
|
|
7
|
+
super()
|
|
8
|
+
|
|
9
|
+
this.addEventListener("mousedown", this)
|
|
10
|
+
window.addEventListener("mousemove", this)
|
|
11
|
+
window.addEventListener("mouseup", this)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
handleEvent(event) {
|
|
15
|
+
switch (event.type) {
|
|
16
|
+
case "mousedown":
|
|
17
|
+
event.stopPropagation()
|
|
18
|
+
this.onMouseDown()
|
|
19
|
+
break
|
|
20
|
+
case "mouseup":
|
|
21
|
+
this.onMouseUp()
|
|
22
|
+
break
|
|
23
|
+
case "mousemove":
|
|
24
|
+
if (this.#dragging) {
|
|
25
|
+
this.onDrag(event.pageX)
|
|
26
|
+
}
|
|
27
|
+
break
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onMouseDown() {
|
|
32
|
+
this.#dragging = true
|
|
33
|
+
this.elementsWindow.isDragged = true
|
|
34
|
+
this.previewWindow.isDragged = true
|
|
35
|
+
this.classList.add("is-dragged")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onMouseUp() {
|
|
39
|
+
this.#dragging = false
|
|
40
|
+
this.elementsWindow.isDragged = false
|
|
41
|
+
this.previewWindow.isDragged = false
|
|
42
|
+
this.classList.remove("is-dragged")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onDrag(pageX) {
|
|
46
|
+
const elementWindowWidth = window.innerWidth - pageX
|
|
47
|
+
this.elementsWindow.resize(elementWindowWidth)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get elementsWindow() {
|
|
51
|
+
if (!this.#elementsWindow) {
|
|
52
|
+
this.#elementsWindow = document.querySelector("alchemy-elements-window")
|
|
53
|
+
}
|
|
54
|
+
return this.#elementsWindow
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get previewWindow() {
|
|
58
|
+
if (!this.#previewWindow) {
|
|
59
|
+
this.#previewWindow = document.getElementById("alchemy_preview_window")
|
|
60
|
+
}
|
|
61
|
+
return this.#previewWindow
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
customElements.define("alchemy-elements-window-handle", ElementsWindowHandle)
|
|
@@ -6,8 +6,8 @@ class Icon extends HTMLElement {
|
|
|
6
6
|
constructor() {
|
|
7
7
|
super()
|
|
8
8
|
this.spriteUrl = document
|
|
9
|
-
.querySelector('
|
|
10
|
-
.getAttribute("
|
|
9
|
+
.querySelector('link[rel="preload"][as="image"]')
|
|
10
|
+
.getAttribute("href")
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
connectedCallback() {
|
|
@@ -8,6 +8,7 @@ import "alchemy_admin/components/dialog_link"
|
|
|
8
8
|
import "alchemy_admin/components/dom_id_select"
|
|
9
9
|
import "alchemy_admin/components/element_editor"
|
|
10
10
|
import "alchemy_admin/components/elements_window"
|
|
11
|
+
import "alchemy_admin/components/elements_window_handle"
|
|
11
12
|
import "alchemy_admin/components/list_filter"
|
|
12
13
|
import "alchemy_admin/components/message"
|
|
13
14
|
import "alchemy_admin/components/growl"
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const MIN_WIDTH = 240
|
|
2
|
-
|
|
3
1
|
class PreviewWindow extends HTMLIFrameElement {
|
|
4
2
|
#afterLoad
|
|
5
3
|
#reloadIcon
|
|
@@ -38,9 +36,6 @@ class PreviewWindow extends HTMLIFrameElement {
|
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
resize(width) {
|
|
41
|
-
if (width < MIN_WIDTH) {
|
|
42
|
-
width = MIN_WIDTH
|
|
43
|
-
}
|
|
44
39
|
this.style.width = `${width}px`
|
|
45
40
|
}
|
|
46
41
|
|
|
@@ -58,6 +53,11 @@ class PreviewWindow extends HTMLIFrameElement {
|
|
|
58
53
|
})
|
|
59
54
|
}
|
|
60
55
|
|
|
56
|
+
set isDragged(dragged) {
|
|
57
|
+
this.style.transitionProperty = dragged ? "none" : null
|
|
58
|
+
this.style.pointerEvents = dragged ? "none" : null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
61
|
#attachEvents() {
|
|
62
62
|
this.reloadButton?.addEventListener("click", (evt) => {
|
|
63
63
|
evt.preventDefault()
|
|
@@ -200,7 +200,7 @@ export class FileUpload extends AlchemyHTMLElement {
|
|
|
200
200
|
const response = JSON.parse(this.request.responseText)
|
|
201
201
|
return response["message"]
|
|
202
202
|
} catch (error) {
|
|
203
|
-
return
|
|
203
|
+
return `${this.request.status}: ${this.request.statusText}`
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
@@ -3,20 +3,18 @@ import pleaseWaitOverlay from "alchemy_admin/please_wait_overlay"
|
|
|
3
3
|
import { createHtmlElement } from "alchemy_admin/utils/dom_helpers"
|
|
4
4
|
import { translate } from "alchemy_admin/i18n"
|
|
5
5
|
|
|
6
|
+
const DEFAULTS = {
|
|
7
|
+
size: "300x100",
|
|
8
|
+
title: translate("Please confirm"),
|
|
9
|
+
ok_label: translate("Yes"),
|
|
10
|
+
cancel_label: translate("No"),
|
|
11
|
+
on_ok() {}
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
class ConfirmDialog {
|
|
7
15
|
constructor(message, options = {}) {
|
|
8
|
-
const DEFAULTS = {
|
|
9
|
-
size: "300x100",
|
|
10
|
-
title: translate("Please confirm"),
|
|
11
|
-
ok_label: translate("Yes"),
|
|
12
|
-
cancel_label: translate("No"),
|
|
13
|
-
on_ok() {}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
options = { ...DEFAULTS, ...options }
|
|
17
|
-
|
|
18
16
|
this.message = message
|
|
19
|
-
this.options = options
|
|
17
|
+
this.options = { ...DEFAULTS, ...options }
|
|
20
18
|
this.#build()
|
|
21
19
|
this.#bindEvents()
|
|
22
20
|
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import Hotkeys from "alchemy_admin/hotkeys"
|
|
2
|
+
import Spinner from "alchemy_admin/spinner"
|
|
3
|
+
|
|
4
|
+
// Collection of all current dialog instances
|
|
5
|
+
const currentDialogs = []
|
|
6
|
+
|
|
7
|
+
const DEFAULTS = {
|
|
8
|
+
header_height: 36,
|
|
9
|
+
size: "400x300",
|
|
10
|
+
padding: true,
|
|
11
|
+
title: "",
|
|
12
|
+
modal: true,
|
|
13
|
+
overflow: "visible",
|
|
14
|
+
ready: () => {},
|
|
15
|
+
closed: () => {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class Dialog {
|
|
19
|
+
// Arguments:
|
|
20
|
+
// - url: The url to load the content from via ajax
|
|
21
|
+
// - options: A object holding options
|
|
22
|
+
// - size: The maximum size of the Dialog
|
|
23
|
+
// - title: The title of the Dialog
|
|
24
|
+
constructor(url, options = {}) {
|
|
25
|
+
this.url = url
|
|
26
|
+
this.options = { ...DEFAULTS, ...options }
|
|
27
|
+
this.$document = $(document)
|
|
28
|
+
this.$window = $(window)
|
|
29
|
+
this.$body = $("body")
|
|
30
|
+
const size = this.options.size.split("x")
|
|
31
|
+
this.width = parseInt(size[0], 10)
|
|
32
|
+
this.height = parseInt(size[1], 10)
|
|
33
|
+
this.build()
|
|
34
|
+
this.resize()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Opens the Dialog and loads the content via ajax.
|
|
38
|
+
open() {
|
|
39
|
+
this.dialog.trigger("Alchemy.DialogOpen")
|
|
40
|
+
this.bind_close_events()
|
|
41
|
+
window.requestAnimationFrame(() => {
|
|
42
|
+
this.dialog_container.addClass("open")
|
|
43
|
+
if (this.overlay != null) {
|
|
44
|
+
return this.overlay.addClass("open")
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
this.$body.addClass("prevent-scrolling")
|
|
48
|
+
currentDialogs.push(this)
|
|
49
|
+
this.load()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Closes the Dialog and removes it from the DOM
|
|
53
|
+
close() {
|
|
54
|
+
this.dialog.trigger("DialogClose.Alchemy")
|
|
55
|
+
this.$document.off("keydown")
|
|
56
|
+
this.dialog_container.removeClass("open")
|
|
57
|
+
if (this.overlay != null) {
|
|
58
|
+
this.overlay.removeClass("open")
|
|
59
|
+
}
|
|
60
|
+
this.$document.on(
|
|
61
|
+
"webkitTransitionEnd transitionend oTransitionEnd",
|
|
62
|
+
() => {
|
|
63
|
+
this.$document.off("webkitTransitionEnd transitionend oTransitionEnd")
|
|
64
|
+
this.dialog_container.remove()
|
|
65
|
+
if (this.overlay != null) {
|
|
66
|
+
this.overlay.remove()
|
|
67
|
+
}
|
|
68
|
+
this.$body.removeClass("prevent-scrolling")
|
|
69
|
+
currentDialogs.pop(this)
|
|
70
|
+
if (this.options.closed != null) {
|
|
71
|
+
return this.options.closed()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Loads the content via ajax and replaces the Dialog body with server response.
|
|
79
|
+
load() {
|
|
80
|
+
this.show_spinner()
|
|
81
|
+
$.get(this.url, (data) => {
|
|
82
|
+
this.replace(data)
|
|
83
|
+
}).fail((xhr) => {
|
|
84
|
+
this.show_error(xhr)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Reloads the Dialog content
|
|
89
|
+
reload() {
|
|
90
|
+
this.dialog_body.empty()
|
|
91
|
+
this.load()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Replaces the dialog body with given content and initializes it.
|
|
95
|
+
replace(data) {
|
|
96
|
+
this.remove_spinner()
|
|
97
|
+
this.dialog_body.hide()
|
|
98
|
+
this.dialog_body.html(data)
|
|
99
|
+
this.init()
|
|
100
|
+
this.dialog[0].dispatchEvent(
|
|
101
|
+
new CustomEvent("DialogReady.Alchemy", {
|
|
102
|
+
bubbles: true,
|
|
103
|
+
detail: {
|
|
104
|
+
body: this.dialog_body[0]
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
)
|
|
108
|
+
if (this.options.ready != null) {
|
|
109
|
+
this.options.ready(this.dialog_body)
|
|
110
|
+
}
|
|
111
|
+
this.dialog_body.show()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Adds a spinner into Dialog body
|
|
115
|
+
show_spinner() {
|
|
116
|
+
this.spinner = new Spinner("medium")
|
|
117
|
+
this.spinner.spin(this.dialog_body[0])
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Removes the spinner from Dialog body
|
|
121
|
+
remove_spinner() {
|
|
122
|
+
this.spinner.stop()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Initializes the Dialog body
|
|
126
|
+
init() {
|
|
127
|
+
Hotkeys(this.dialog_body)
|
|
128
|
+
this.watch_remote_forms()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Watches ajax requests inside of dialog body and replaces the content accordingly
|
|
132
|
+
watch_remote_forms() {
|
|
133
|
+
const $form = $('[data-remote="true"]', this.dialog_body)
|
|
134
|
+
|
|
135
|
+
$form.on("ajax:success", (event) => {
|
|
136
|
+
const xhr = event.detail[2]
|
|
137
|
+
const content_type = xhr.getResponseHeader("Content-Type")
|
|
138
|
+
if (content_type.match(/javascript/)) {
|
|
139
|
+
return
|
|
140
|
+
} else {
|
|
141
|
+
this.dialog_body.html(xhr.responseText)
|
|
142
|
+
this.init()
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
$form.on("ajax:error", (event) => {
|
|
147
|
+
const statusText = event.detail[1]
|
|
148
|
+
const xhr = event.detail[2]
|
|
149
|
+
this.show_error(xhr, statusText)
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Displays an error message
|
|
154
|
+
show_error(xhr, statusText) {
|
|
155
|
+
if (xhr.status === 422) {
|
|
156
|
+
this.dialog_body.html(xhr.responseText)
|
|
157
|
+
this.init()
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { error_body, error_header, error_type } = this.error_messages(
|
|
162
|
+
xhr,
|
|
163
|
+
statusText
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
const $errorDiv = $(`<alchemy-message type="${error_type}">
|
|
167
|
+
<h1>${error_header}</h1>
|
|
168
|
+
<p>${error_body}</p>
|
|
169
|
+
</alchemy-message>`)
|
|
170
|
+
|
|
171
|
+
this.dialog_body.html($errorDiv)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Returns error message based on xhr status
|
|
175
|
+
error_messages(xhr, statusText) {
|
|
176
|
+
let error_body,
|
|
177
|
+
error_header,
|
|
178
|
+
error_type = "warning"
|
|
179
|
+
|
|
180
|
+
switch (xhr.status) {
|
|
181
|
+
case 0:
|
|
182
|
+
error_header = "The server does not respond."
|
|
183
|
+
error_body = "Please check server and try again."
|
|
184
|
+
break
|
|
185
|
+
case 403:
|
|
186
|
+
error_header = "You are not authorized!"
|
|
187
|
+
error_body = "Please close this window."
|
|
188
|
+
break
|
|
189
|
+
default:
|
|
190
|
+
error_type = "error"
|
|
191
|
+
if (statusText) {
|
|
192
|
+
error_header = statusText
|
|
193
|
+
console.error(xhr.responseText)
|
|
194
|
+
} else {
|
|
195
|
+
error_header = `${xhr.statusText} (${xhr.status})`
|
|
196
|
+
}
|
|
197
|
+
error_body = "Please check log and try again."
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { error_header, error_body, error_type }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Binds close events on:
|
|
204
|
+
// - Close button
|
|
205
|
+
// - Overlay (if the Dialog is a modal)
|
|
206
|
+
// - ESC Key
|
|
207
|
+
bind_close_events() {
|
|
208
|
+
this.close_button.on("click", () => {
|
|
209
|
+
this.close()
|
|
210
|
+
})
|
|
211
|
+
this.dialog_container.addClass("closable").on("click", (e) => {
|
|
212
|
+
if (e.target !== this.dialog_container.get(0)) {
|
|
213
|
+
return true
|
|
214
|
+
}
|
|
215
|
+
this.close()
|
|
216
|
+
return false
|
|
217
|
+
})
|
|
218
|
+
this.$document.keydown((e) => {
|
|
219
|
+
if (e.which === 27) {
|
|
220
|
+
this.close()
|
|
221
|
+
return false
|
|
222
|
+
} else {
|
|
223
|
+
return true
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Builds the html structure of the Dialog
|
|
229
|
+
build() {
|
|
230
|
+
this.dialog_container = $('<div class="alchemy-dialog-container" />')
|
|
231
|
+
this.dialog = $('<div class="alchemy-dialog" />')
|
|
232
|
+
this.dialog_body = $('<div class="alchemy-dialog-body" />')
|
|
233
|
+
this.dialog_header = $('<div class="alchemy-dialog-header" />')
|
|
234
|
+
this.dialog_title = $('<div class="alchemy-dialog-title" />')
|
|
235
|
+
this.close_button = $(
|
|
236
|
+
'<a class="alchemy-dialog-close"><alchemy-icon name="close"></alchemy-icon></a>'
|
|
237
|
+
)
|
|
238
|
+
this.dialog_title.text(this.options.title)
|
|
239
|
+
this.dialog_header.append(this.dialog_title)
|
|
240
|
+
this.dialog_header.append(this.close_button)
|
|
241
|
+
this.dialog.append(this.dialog_header)
|
|
242
|
+
this.dialog.append(this.dialog_body)
|
|
243
|
+
this.dialog_container.append(this.dialog)
|
|
244
|
+
if (this.options.modal) {
|
|
245
|
+
this.dialog.addClass("modal")
|
|
246
|
+
}
|
|
247
|
+
if (this.options.padding) {
|
|
248
|
+
this.dialog_body.addClass("padded")
|
|
249
|
+
}
|
|
250
|
+
if (this.options.modal) {
|
|
251
|
+
this.overlay = $('<div class="alchemy-dialog-overlay" />')
|
|
252
|
+
this.$body.append(this.overlay)
|
|
253
|
+
}
|
|
254
|
+
this.$body.append(this.dialog_container)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Sets the correct size of the dialog
|
|
258
|
+
// It normalizes the given size, so that it never acceeds the window size.
|
|
259
|
+
resize() {
|
|
260
|
+
const { width, height } = this.getSize()
|
|
261
|
+
|
|
262
|
+
this.dialog.css({
|
|
263
|
+
width: width,
|
|
264
|
+
"min-height": height,
|
|
265
|
+
overflow: this.options.overflow
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
if (this.options.overflow === "hidden") {
|
|
269
|
+
this.dialog_body.css({
|
|
270
|
+
height: height,
|
|
271
|
+
overflow: "auto"
|
|
272
|
+
})
|
|
273
|
+
} else {
|
|
274
|
+
this.dialog_body.css({
|
|
275
|
+
"min-height": height,
|
|
276
|
+
overflow: "visible"
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
getSize() {
|
|
282
|
+
const padding = this.options.padding ? 16 : 0
|
|
283
|
+
const doc_width = this.$window.width()
|
|
284
|
+
const doc_height = this.$window.height()
|
|
285
|
+
|
|
286
|
+
let width = this.width
|
|
287
|
+
let height = this.height
|
|
288
|
+
|
|
289
|
+
if (width >= doc_width) {
|
|
290
|
+
width = doc_width - padding
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (height >= doc_height) {
|
|
294
|
+
height = doc_height - padding - DEFAULTS.header_height
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return { width, height }
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Gets the last dialog instantiated, which is the current one.
|
|
302
|
+
export function currentDialog() {
|
|
303
|
+
const { length } = currentDialogs
|
|
304
|
+
if (length === 0) {
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
return currentDialogs[length - 1]
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Utility function to close the current Dialog
|
|
311
|
+
//
|
|
312
|
+
// You can pass a callback function, that gets triggered after the Dialog gets closed.
|
|
313
|
+
//
|
|
314
|
+
export function closeCurrentDialog(callback) {
|
|
315
|
+
const dialog = currentDialog()
|
|
316
|
+
if (dialog != null) {
|
|
317
|
+
dialog.options.closed = callback
|
|
318
|
+
return dialog.close()
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Utility function to open a new Dialog
|
|
323
|
+
export function openDialog(url, options) {
|
|
324
|
+
if (!url) {
|
|
325
|
+
throw "No url given! Please provide an url."
|
|
326
|
+
}
|
|
327
|
+
const dialog = new Dialog(url, options)
|
|
328
|
+
dialog.open()
|
|
329
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "keymaster"
|
|
2
|
+
import { openDialog } from "alchemy_admin/dialog"
|
|
2
3
|
|
|
3
4
|
const bindedHotkeys = []
|
|
4
5
|
|
|
@@ -7,7 +8,7 @@ function showHelp(evt) {
|
|
|
7
8
|
!$(evt.target).is("input, textarea") &&
|
|
8
9
|
String.fromCharCode(evt.which) === "?"
|
|
9
10
|
) {
|
|
10
|
-
|
|
11
|
+
openDialog("/admin/help", {
|
|
11
12
|
title: Alchemy.t("help"),
|
|
12
13
|
size: "400x492"
|
|
13
14
|
})
|
|
@@ -18,7 +19,7 @@ function showHelp(evt) {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export default function (scope = document) {
|
|
21
|
-
// The scope can be a jQuery object because we still use jQuery in
|
|
22
|
+
// The scope can be a jQuery object because we still use jQuery in alchemy_admin/dialog.js.
|
|
22
23
|
if (scope instanceof jQuery) {
|
|
23
24
|
scope = scope[0]
|
|
24
25
|
}
|