alchemy_cms 7.3.5 → 7.4.1
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 +60 -0
- data/Gemfile +3 -3
- data/README.md +2 -2
- 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/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 +8 -3
- data/app/controllers/alchemy/admin/elements_controller.rb +2 -2
- data/app/controllers/alchemy/admin/layoutpages_controller.rb +1 -0
- data/app/controllers/alchemy/admin/pages_controller.rb +5 -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 +56 -48
- 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/_preview_mode_code.html.erb +1 -1
- data/app/views/alchemy/admin/crop.html.erb +18 -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/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 -92
- 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
@@ -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
|
}
|
@@ -1,91 +1,99 @@
|
|
1
|
+
import Cropper from "cropperjs"
|
2
|
+
|
1
3
|
export default class ImageCropper {
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
trueSize,
|
7
|
-
formFieldIds,
|
8
|
-
elementId
|
9
|
-
) {
|
10
|
-
this.initialized = false
|
4
|
+
#initialized = false
|
5
|
+
#cropper = null
|
6
|
+
#cropFromField = null
|
7
|
+
#cropSizeField = null
|
11
8
|
|
12
|
-
|
9
|
+
constructor(image, defaultBox, aspectRatio, formFieldIds, elementId) {
|
10
|
+
this.image = image
|
13
11
|
this.defaultBox = defaultBox
|
14
12
|
this.aspectRatio = aspectRatio
|
15
|
-
this
|
16
|
-
this
|
17
|
-
this.cropSizeField = document.getElementById(formFieldIds[1])
|
13
|
+
this.#cropFromField = document.getElementById(formFieldIds[0])
|
14
|
+
this.#cropSizeField = document.getElementById(formFieldIds[1])
|
18
15
|
this.elementId = elementId
|
19
16
|
this.dialog = Alchemy.currentDialog()
|
20
|
-
this.dialog
|
21
|
-
|
17
|
+
if (this.dialog) {
|
18
|
+
this.dialog.options.closed = () => this.destroy()
|
19
|
+
this.bind()
|
20
|
+
}
|
22
21
|
this.init()
|
23
|
-
this.bind()
|
24
22
|
}
|
25
23
|
|
26
|
-
get
|
24
|
+
get cropperOptions() {
|
27
25
|
return {
|
28
|
-
onSelect: this.update.bind(this),
|
29
|
-
setSelect: this.box,
|
30
26
|
aspectRatio: this.aspectRatio,
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
27
|
+
viewMode: 1,
|
28
|
+
zoomable: false,
|
29
|
+
checkCrossOrigin: false, // Prevent CORS issues
|
30
|
+
checkOrientation: false, // Prevent loading the image via AJAX which can cause CORS issues
|
31
|
+
data: this.box,
|
32
|
+
cropend: () => {
|
33
|
+
const data = this.#cropper.getData(true)
|
34
|
+
this.update(data)
|
35
|
+
}
|
36
36
|
}
|
37
37
|
}
|
38
38
|
|
39
39
|
get cropFrom() {
|
40
|
-
if (this
|
41
|
-
return this
|
40
|
+
if (this.#cropFromField?.value) {
|
41
|
+
return this.#cropFromField.value.split("x").map((v) => parseInt(v))
|
42
42
|
}
|
43
43
|
}
|
44
44
|
|
45
45
|
get cropSize() {
|
46
|
-
if (this
|
47
|
-
return this
|
46
|
+
if (this.#cropSizeField?.value) {
|
47
|
+
return this.#cropSizeField.value.split("x").map((v) => parseInt(v))
|
48
48
|
}
|
49
49
|
}
|
50
50
|
|
51
51
|
get box() {
|
52
52
|
if (this.cropFrom && this.cropSize) {
|
53
|
-
return
|
54
|
-
this.cropFrom[0],
|
55
|
-
this.cropFrom[1],
|
56
|
-
|
57
|
-
|
58
|
-
|
53
|
+
return {
|
54
|
+
x: this.cropFrom[0],
|
55
|
+
y: this.cropFrom[1],
|
56
|
+
width: this.cropSize[0],
|
57
|
+
height: this.cropSize[1]
|
58
|
+
}
|
59
59
|
} else {
|
60
|
-
return this.
|
60
|
+
return this.defaultBoxSize
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
get defaultBoxSize() {
|
65
|
+
return {
|
66
|
+
x: this.defaultBox[0],
|
67
|
+
y: this.defaultBox[1],
|
68
|
+
width: this.defaultBox[2],
|
69
|
+
height: this.defaultBox[3]
|
61
70
|
}
|
62
71
|
}
|
63
72
|
|
64
73
|
init() {
|
65
|
-
if (!this
|
66
|
-
this
|
67
|
-
this
|
74
|
+
if (!this.#initialized) {
|
75
|
+
this.#cropper = new Cropper(this.image, this.cropperOptions)
|
76
|
+
this.#initialized = true
|
68
77
|
}
|
69
78
|
}
|
70
79
|
|
71
80
|
update(coords) {
|
72
|
-
this
|
73
|
-
this
|
74
|
-
this
|
75
|
-
this.
|
81
|
+
this.#cropFromField.value = `${coords.x}x${coords.y}`
|
82
|
+
this.#cropFromField.dispatchEvent(new Event("change"))
|
83
|
+
this.#cropSizeField.value = `${coords.width}x${coords.height}`
|
84
|
+
this.#cropSizeField.dispatchEvent(new Event("change"))
|
76
85
|
}
|
77
86
|
|
78
87
|
reset() {
|
79
|
-
this.
|
80
|
-
this.
|
81
|
-
this.cropSizeField.value = `${this.box[2]}x${this.box[3] - this.box[1]}`
|
88
|
+
this.#cropper.setData(this.defaultBoxSize)
|
89
|
+
this.update(this.defaultBoxSize)
|
82
90
|
}
|
83
91
|
|
84
92
|
destroy() {
|
85
|
-
if (this
|
86
|
-
this.
|
93
|
+
if (this.#cropper) {
|
94
|
+
this.#cropper.destroy()
|
87
95
|
}
|
88
|
-
this
|
96
|
+
this.#initialized = false
|
89
97
|
return true
|
90
98
|
}
|
91
99
|
|