alchemy_cms 7.3.6 → 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 +55 -6
- 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 +8 -0
- 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/admin/link_dialog/internal_tab.rb +1 -2
- 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 -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
@@ -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
|
|
@@ -0,0 +1,73 @@
|
|
1
|
+
import ImageLoader from "alchemy_admin/image_loader"
|
2
|
+
import { Dialog } from "alchemy_admin/dialog"
|
3
|
+
|
4
|
+
export default class ImageOverlay extends Dialog {
|
5
|
+
constructor(url, options = {}) {
|
6
|
+
super(url, options)
|
7
|
+
}
|
8
|
+
|
9
|
+
init() {
|
10
|
+
ImageLoader.init(this.dialog_body[0])
|
11
|
+
$(".zoomed-picture-background").on("click", (e) => {
|
12
|
+
e.stopPropagation()
|
13
|
+
if (e.target.nodeName === "IMG") {
|
14
|
+
return
|
15
|
+
}
|
16
|
+
this.close()
|
17
|
+
return false
|
18
|
+
})
|
19
|
+
$(".picture-overlay-handle").on("click", (e) => {
|
20
|
+
this.dialog.toggleClass("hide-form")
|
21
|
+
return false
|
22
|
+
})
|
23
|
+
this.$previous = $(".previous-picture")
|
24
|
+
this.$next = $(".next-picture")
|
25
|
+
this.#initKeyboardNavigation()
|
26
|
+
super.init()
|
27
|
+
}
|
28
|
+
|
29
|
+
previous() {
|
30
|
+
if (this.$previous[0] != null) {
|
31
|
+
this.$previous[0].click()
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
next() {
|
36
|
+
if (this.$next[0] != null) {
|
37
|
+
this.$next[0].click()
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
build() {
|
42
|
+
this.dialog_container = $('<div class="alchemy-image-overlay-container" />')
|
43
|
+
this.dialog = $('<div class="alchemy-image-overlay-dialog" />')
|
44
|
+
this.dialog_body = $('<div class="alchemy-image-overlay-body" />')
|
45
|
+
this.close_button = $(`<a class="alchemy-image-overlay-close">
|
46
|
+
<alchemy-icon name="close" size="xl"></alchemy-icon>
|
47
|
+
</a>`)
|
48
|
+
this.dialog.append(this.close_button)
|
49
|
+
this.dialog.append(this.dialog_body)
|
50
|
+
this.dialog_container.append(this.dialog)
|
51
|
+
this.overlay = $('<div class="alchemy-image-overlay" />')
|
52
|
+
this.$body.append(this.overlay)
|
53
|
+
this.$body.append(this.dialog_container)
|
54
|
+
}
|
55
|
+
|
56
|
+
#initKeyboardNavigation() {
|
57
|
+
this.$document.keydown((e) => {
|
58
|
+
if (e.target.nodeName === "INPUT" || e.target.nodeName === "TEXTAREA") {
|
59
|
+
return true
|
60
|
+
}
|
61
|
+
switch (e.which) {
|
62
|
+
case 37:
|
63
|
+
this.previous()
|
64
|
+
return false
|
65
|
+
case 39:
|
66
|
+
this.next()
|
67
|
+
return false
|
68
|
+
default:
|
69
|
+
return true
|
70
|
+
}
|
71
|
+
})
|
72
|
+
}
|
73
|
+
}
|
@@ -1,3 +1,11 @@
|
|
1
|
+
import {
|
2
|
+
confirmToDeleteDialog,
|
3
|
+
openConfirmDialog
|
4
|
+
} from "alchemy_admin/confirm_dialog"
|
5
|
+
|
6
|
+
import Hotkeys from "alchemy_admin/hotkeys"
|
7
|
+
import pleaseWaitOverlay from "alchemy_admin/please_wait_overlay"
|
8
|
+
|
1
9
|
/**
|
2
10
|
* add change listener to select to redirect the user after selecting another locale or site
|
3
11
|
* @param {string} selectId
|
@@ -18,12 +26,53 @@ function selectHandler(selectId, parameterName, forcedReload = false) {
|
|
18
26
|
})
|
19
27
|
}
|
20
28
|
|
29
|
+
// Watches elements for Alchemy Dialogs
|
30
|
+
//
|
31
|
+
// Links having a data-alchemy-confirm-delete
|
32
|
+
// and input/buttons having a data-alchemy-confirm attribute get watched.
|
33
|
+
//
|
34
|
+
// You can pass a scope so that only elements inside this scope are queried.
|
35
|
+
//
|
36
|
+
// The href attribute of the link is the url for the overlay window.
|
37
|
+
//
|
38
|
+
// See Dialog for further options you can add to the data attribute.
|
39
|
+
//
|
40
|
+
function watchForConfirmDialogs(scope) {
|
41
|
+
if (scope == null) {
|
42
|
+
scope = "#alchemy"
|
43
|
+
}
|
44
|
+
$(scope).on("click", "[data-alchemy-confirm-delete]", function (event) {
|
45
|
+
const $this = $(this)
|
46
|
+
const options = $this.data("alchemy-confirm-delete")
|
47
|
+
confirmToDeleteDialog($this.attr("href"), options)
|
48
|
+
event.preventDefault()
|
49
|
+
})
|
50
|
+
$(scope).on("click", "[data-alchemy-confirm]", function (event) {
|
51
|
+
const options = $(this).data("alchemy-confirm")
|
52
|
+
openConfirmDialog(
|
53
|
+
options.message,
|
54
|
+
$.extend(options, {
|
55
|
+
ok_label: options.ok_label,
|
56
|
+
cancel_label: options.cancel_label,
|
57
|
+
on_ok: () => {
|
58
|
+
pleaseWaitOverlay()
|
59
|
+
this.form.submit()
|
60
|
+
}
|
61
|
+
})
|
62
|
+
)
|
63
|
+
event.preventDefault()
|
64
|
+
})
|
65
|
+
}
|
66
|
+
|
21
67
|
export default function Initializer() {
|
22
68
|
// We obviously have javascript enabled.
|
23
69
|
$("html").removeClass("no-js")
|
24
70
|
|
25
|
-
// Initialize
|
26
|
-
|
71
|
+
// Initialize hotkeys.
|
72
|
+
Hotkeys()
|
73
|
+
|
74
|
+
// Watch for click on confirm dialog links.
|
75
|
+
watchForConfirmDialogs()
|
27
76
|
|
28
77
|
// Add observer for please wait overlay.
|
29
78
|
$(".please_wait")
|
@@ -1,9 +1,10 @@
|
|
1
1
|
import { translate } from "alchemy_admin/i18n"
|
2
|
+
import { Dialog } from "alchemy_admin/dialog"
|
2
3
|
|
3
4
|
// Represents the link Dialog that appears, if a user clicks the link buttons
|
4
5
|
// in TinyMCE or on an Ingredient that has links enabled (e.g. Picture)
|
5
6
|
//
|
6
|
-
export class LinkDialog extends
|
7
|
+
export class LinkDialog extends Dialog {
|
7
8
|
#onCreateLink
|
8
9
|
|
9
10
|
constructor(link) {
|
@@ -14,7 +14,9 @@ function displayNodeFolders() {
|
|
14
14
|
}
|
15
15
|
|
16
16
|
if (list.children.length > 0 || node.folded) {
|
17
|
-
leftIconArea.innerHTML =
|
17
|
+
leftIconArea.innerHTML = Handlebars.templates["node_folder.hbs"]({
|
18
|
+
node: node
|
19
|
+
})
|
18
20
|
} else {
|
19
21
|
leftIconArea.innerHTML = " "
|
20
22
|
}
|