alchemy_cms 7.3.5 → 7.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +56 -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 +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/_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/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 +39 -91
- 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,108 @@
|
|
1
|
+
import Cropper from "cropperjs"
|
2
|
+
|
1
3
|
export default class ImageCropper {
|
4
|
+
#initialized = false
|
5
|
+
#cropper = null
|
6
|
+
#cropFromField = null
|
7
|
+
#cropSizeField = null
|
8
|
+
|
2
9
|
constructor(
|
10
|
+
image,
|
3
11
|
minSize,
|
4
12
|
defaultBox,
|
5
13
|
aspectRatio,
|
6
|
-
trueSize,
|
7
14
|
formFieldIds,
|
8
15
|
elementId
|
9
16
|
) {
|
10
|
-
this.
|
11
|
-
|
17
|
+
this.image = image
|
12
18
|
this.minSize = minSize
|
13
19
|
this.defaultBox = defaultBox
|
14
20
|
this.aspectRatio = aspectRatio
|
15
|
-
this
|
16
|
-
this
|
17
|
-
this.cropSizeField = document.getElementById(formFieldIds[1])
|
21
|
+
this.#cropFromField = document.getElementById(formFieldIds[0])
|
22
|
+
this.#cropSizeField = document.getElementById(formFieldIds[1])
|
18
23
|
this.elementId = elementId
|
19
24
|
this.dialog = Alchemy.currentDialog()
|
20
|
-
this.dialog.options.closed = this.destroy
|
21
|
-
|
25
|
+
this.dialog.options.closed = () => this.destroy()
|
22
26
|
this.init()
|
23
27
|
this.bind()
|
24
28
|
}
|
25
29
|
|
26
|
-
get
|
30
|
+
get cropperOptions() {
|
27
31
|
return {
|
28
|
-
onSelect: this.update.bind(this),
|
29
|
-
setSelect: this.box,
|
30
32
|
aspectRatio: this.aspectRatio,
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
viewMode: 1,
|
34
|
+
zoomable: false,
|
35
|
+
minCropBoxWidth: this.minSize && this.minSize[0],
|
36
|
+
minCropBoxHeight: this.minSize && this.minSize[1],
|
37
|
+
ready: (event) => {
|
38
|
+
const cropper = event.target.cropper
|
39
|
+
cropper.setData(this.box)
|
40
|
+
},
|
41
|
+
cropend: () => {
|
42
|
+
const data = this.#cropper.getData(true)
|
43
|
+
this.update(data)
|
44
|
+
}
|
36
45
|
}
|
37
46
|
}
|
38
47
|
|
39
48
|
get cropFrom() {
|
40
|
-
if (this
|
41
|
-
return this
|
49
|
+
if (this.#cropFromField?.value) {
|
50
|
+
return this.#cropFromField.value.split("x").map((v) => parseInt(v))
|
42
51
|
}
|
43
52
|
}
|
44
53
|
|
45
54
|
get cropSize() {
|
46
|
-
if (this
|
47
|
-
return this
|
55
|
+
if (this.#cropSizeField?.value) {
|
56
|
+
return this.#cropSizeField.value.split("x").map((v) => parseInt(v))
|
48
57
|
}
|
49
58
|
}
|
50
59
|
|
51
60
|
get box() {
|
52
61
|
if (this.cropFrom && this.cropSize) {
|
53
|
-
return
|
54
|
-
this.cropFrom[0],
|
55
|
-
this.cropFrom[1],
|
56
|
-
|
57
|
-
|
58
|
-
|
62
|
+
return {
|
63
|
+
x: this.cropFrom[0],
|
64
|
+
y: this.cropFrom[1],
|
65
|
+
width: this.cropSize[0],
|
66
|
+
height: this.cropSize[1]
|
67
|
+
}
|
59
68
|
} else {
|
60
|
-
return this.
|
69
|
+
return this.defaultBoxSize
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
get defaultBoxSize() {
|
74
|
+
return {
|
75
|
+
x: this.defaultBox[0],
|
76
|
+
y: this.defaultBox[1],
|
77
|
+
width: this.defaultBox[2],
|
78
|
+
height: this.defaultBox[3]
|
61
79
|
}
|
62
80
|
}
|
63
81
|
|
64
82
|
init() {
|
65
|
-
if (!this
|
66
|
-
this
|
67
|
-
this
|
83
|
+
if (!this.#initialized) {
|
84
|
+
this.#cropper = new Cropper(this.image, this.cropperOptions)
|
85
|
+
this.#initialized = true
|
68
86
|
}
|
69
87
|
}
|
70
88
|
|
71
89
|
update(coords) {
|
72
|
-
this
|
73
|
-
this
|
74
|
-
this
|
75
|
-
this.
|
90
|
+
this.#cropFromField.value = `${coords.x}x${coords.y}`
|
91
|
+
this.#cropFromField.dispatchEvent(new Event("change"))
|
92
|
+
this.#cropSizeField.value = `${coords.width}x${coords.height}`
|
93
|
+
this.#cropSizeField.dispatchEvent(new Event("change"))
|
76
94
|
}
|
77
95
|
|
78
96
|
reset() {
|
79
|
-
this.
|
80
|
-
this.
|
81
|
-
this.cropSizeField.value = `${this.box[2]}x${this.box[3] - this.box[1]}`
|
97
|
+
this.#cropper.setData(this.defaultBoxSize)
|
98
|
+
this.update(this.defaultBoxSize)
|
82
99
|
}
|
83
100
|
|
84
101
|
destroy() {
|
85
|
-
if (this
|
86
|
-
this.
|
102
|
+
if (this.#cropper) {
|
103
|
+
this.#cropper.destroy()
|
87
104
|
}
|
88
|
-
this
|
105
|
+
this.#initialized = false
|
89
106
|
return true
|
90
107
|
}
|
91
108
|
|