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.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -6
  3. data/Gemfile +3 -3
  4. data/README.md +2 -2
  5. data/alchemy_cms.gemspec +1 -4
  6. data/app/assets/builds/alchemy/admin.css +9 -1
  7. data/app/assets/builds/alchemy/admin.css.map +1 -1
  8. data/app/assets/builds/alchemy/custom-properties.css +1 -1
  9. data/app/assets/builds/alchemy/custom-properties.css.map +1 -1
  10. data/app/assets/builds/alchemy/preview.min.js +1 -0
  11. data/app/assets/builds/alchemy/welcome.css +1 -1
  12. data/app/assets/builds/alchemy/welcome.css.map +1 -1
  13. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  14. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css.map +1 -1
  15. data/app/assets/config/alchemy_manifest.js +0 -4
  16. data/app/assets/javascripts/alchemy/admin.js +8 -6
  17. data/app/assets/stylesheets/alchemy/admin/elements.scss +43 -7
  18. data/app/assets/stylesheets/alchemy/admin/forms.scss +4 -0
  19. data/app/assets/stylesheets/alchemy/admin/navigation.scss +8 -0
  20. data/app/assets/stylesheets/alchemy/admin/preview_window.scss +22 -17
  21. data/app/assets/stylesheets/alchemy/admin.scss +1 -1
  22. data/app/assets/stylesheets/alchemy/custom-properties.css +2 -1
  23. data/app/components/alchemy/admin/link_dialog/internal_tab.rb +1 -2
  24. data/app/controllers/alchemy/admin/base_controller.rb +8 -3
  25. data/app/controllers/alchemy/admin/elements_controller.rb +2 -2
  26. data/app/controllers/alchemy/admin/layoutpages_controller.rb +1 -0
  27. data/app/controllers/alchemy/admin/pages_controller.rb +5 -1
  28. data/app/controllers/alchemy/elements_controller.rb +3 -0
  29. data/app/helpers/alchemy/admin/form_helper.rb +1 -1
  30. data/app/helpers/alchemy/admin/navigation_helper.rb +22 -1
  31. data/app/javascript/alchemy_admin/components/action.js +2 -1
  32. data/app/javascript/alchemy_admin/components/dialog_link.js +3 -18
  33. data/app/javascript/alchemy_admin/components/element_editor.js +9 -0
  34. data/app/javascript/alchemy_admin/components/elements_window.js +34 -0
  35. data/app/javascript/alchemy_admin/components/elements_window_handle.js +65 -0
  36. data/app/javascript/alchemy_admin/components/icon.js +2 -2
  37. data/app/javascript/alchemy_admin/components/index.js +1 -0
  38. data/app/javascript/alchemy_admin/components/preview_window.js +5 -5
  39. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +1 -1
  40. data/app/javascript/alchemy_admin/confirm_dialog.js +9 -11
  41. data/app/javascript/alchemy_admin/dialog.js +329 -0
  42. data/app/javascript/alchemy_admin/hotkeys.js +3 -2
  43. data/app/javascript/alchemy_admin/image_cropper.js +57 -40
  44. data/app/javascript/alchemy_admin/image_overlay.js +73 -0
  45. data/app/javascript/alchemy_admin/initializer.js +51 -2
  46. data/app/javascript/alchemy_admin/link_dialog.js +2 -1
  47. data/app/javascript/alchemy_admin/node_tree.js +3 -1
  48. data/app/javascript/alchemy_admin/page_sorter.js +1 -1
  49. data/app/javascript/alchemy_admin/picture_selector.js +2 -1
  50. data/app/javascript/alchemy_admin/shoelace_theme.js +2 -2
  51. data/app/javascript/alchemy_admin/templates/compiled.js +1 -0
  52. data/app/javascript/alchemy_admin.js +10 -6
  53. data/app/javascript/preview.js +117 -0
  54. data/app/models/alchemy/image_cropper_settings.rb +3 -4
  55. data/app/views/alchemy/_preview_mode_code.html.erb +1 -1
  56. data/app/views/alchemy/admin/crop.html.erb +19 -16
  57. data/app/views/alchemy/admin/dashboard/info.html.erb +1 -1
  58. data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +9 -8
  59. data/app/views/alchemy/admin/elements/_clipboard_button.html.erb +14 -0
  60. data/app/views/alchemy/admin/elements/_element.html.erb +2 -0
  61. data/app/views/alchemy/admin/elements/_form.html.erb +15 -13
  62. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +34 -0
  63. data/app/views/alchemy/admin/elements/index.html.erb +3 -15
  64. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +1 -1
  65. data/app/views/alchemy/admin/layoutpages/edit.html.erb +7 -5
  66. data/app/views/alchemy/admin/nodes/_form.html.erb +1 -1
  67. data/app/views/alchemy/admin/pages/_current_page.html.erb +1 -1
  68. data/app/views/alchemy/admin/pages/_form.html.erb +43 -40
  69. data/app/views/alchemy/admin/pages/_locked_page.html.erb +1 -1
  70. data/app/views/alchemy/admin/pages/_page_layout_filter.html.erb +1 -1
  71. data/app/views/alchemy/admin/pages/_sitemap.html.erb +1 -1
  72. data/app/views/alchemy/admin/pages/_table.html.erb +2 -2
  73. data/app/views/alchemy/admin/pages/edit.html.erb +1 -1
  74. data/app/views/alchemy/admin/pages/update.turbo_stream.erb +39 -0
  75. data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +3 -4
  76. data/app/views/alchemy/admin/pictures/_picture_description_field.html.erb +7 -5
  77. data/app/views/alchemy/admin/pictures/index.html.erb +13 -9
  78. data/app/views/alchemy/admin/resources/_filter_bar.html.erb +1 -1
  79. data/app/views/layouts/alchemy/admin.html.erb +8 -4
  80. data/bun.lockb +0 -0
  81. data/bundles/tinymce.js +2 -0
  82. data/config/alchemy/config.yml +3 -3
  83. data/config/alchemy/modules.yml +7 -6
  84. data/config/importmap.rb +4 -0
  85. data/config/routes.rb +1 -1
  86. data/lib/alchemy/engine.rb +6 -0
  87. data/lib/alchemy/modules.rb +0 -27
  88. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +10 -10
  89. data/lib/alchemy/tinymce.rb +2 -1
  90. data/lib/alchemy/upgrader/seven_point_four.rb +26 -0
  91. data/lib/alchemy/version.rb +1 -1
  92. data/lib/alchemy.rb +14 -0
  93. data/lib/alchemy_cms.rb +0 -2
  94. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +5 -0
  95. data/lib/generators/alchemy/ingredient/templates/view.html.erb +1 -1
  96. data/lib/generators/alchemy/ingredient/templates/view_component.rb.tt +10 -0
  97. data/lib/generators/alchemy/install/install_generator.rb +0 -1
  98. data/lib/generators/alchemy/install/templates/elements.yml.tt +1 -1
  99. data/lib/tasks/alchemy/upgrade.rake +19 -20
  100. data/rollup.config.mjs +44 -1
  101. data/vendor/javascript/cropperjs.min.js +10 -0
  102. data/vendor/javascript/handlebars.min.js +29 -0
  103. data/vendor/javascript/jquery.min.js +2 -0
  104. data/vendor/javascript/select2.min.js +23 -0
  105. data/vendor/javascript/tinymce.min.js +1 -1
  106. metadata +39 -92
  107. data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +0 -271
  108. data/app/assets/javascripts/alchemy/alchemy.image_overlay.coffee +0 -54
  109. data/app/assets/javascripts/alchemy/alchemy.preview.js.coffee +0 -97
  110. data/app/assets/javascripts/alchemy/preview.js +0 -1
  111. data/app/assets/javascripts/alchemy/templates/index.js +0 -2
  112. data/app/javascript/alchemy_admin/gui.js +0 -12
  113. data/app/views/alchemy/admin/elements/create.js.erb +0 -35
  114. data/app/views/alchemy/admin/pages/update.js.erb +0 -43
  115. data/lib/alchemy/upgrader/seven_point_zero.rb +0 -36
  116. data/vendor/assets/images/Jcrop.gif +0 -0
  117. data/vendor/assets/javascripts/jquery_plugins/jquery.Jcrop.min.js +0 -7
  118. data/vendor/assets/javascripts/jquery_plugins/select2.js +0 -3729
  119. data/vendor/assets/stylesheets/jquery.Jcrop.min.css +0 -2
  120. data/vendor/assets/stylesheets/tinymce/skins/content/default/content.min.css +0 -1
  121. /data/app/{assets/javascripts/alchemy → javascript/alchemy_admin}/templates/node_folder.hbs +0 -0
  122. /data/app/{assets/javascripts/alchemy → javascript/alchemy_admin}/templates/page_folder.hbs +0 -0
  123. /data/app/{assets/javascripts/tinymce/icons/remixicons/icons.js → javascript/tinymce/icons/remixicons/index.js} +0 -0
  124. /data/app/{assets/javascripts/tinymce/plugins/alchemy_link/plugin.min.js → javascript/tinymce/plugins/alchemy_link/index.js} +0 -0
  125. /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
- Alchemy.openDialog("/admin/help", {
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 Alchemy.Dialog.
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.initialized = false
11
-
17
+ this.image = image
12
18
  this.minSize = minSize
13
19
  this.defaultBox = defaultBox
14
20
  this.aspectRatio = aspectRatio
15
- this.trueSize = trueSize
16
- this.cropFromField = document.getElementById(formFieldIds[0])
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 jcropOptions() {
30
+ get cropperOptions() {
27
31
  return {
28
- onSelect: this.update.bind(this),
29
- setSelect: this.box,
30
32
  aspectRatio: this.aspectRatio,
31
- minSize: this.minSize,
32
- boxWidth: 800,
33
- boxHeight: 600,
34
- trueSize: this.trueSize,
35
- closed: this.destroy.bind(this)
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.cropFromField.value) {
41
- return this.cropFromField.value.split("x").map((v) => parseInt(v))
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.cropSizeField.value) {
47
- return this.cropSizeField.value.split("x").map((v) => parseInt(v))
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
- this.cropFrom[0] + this.cropSize[0],
57
- this.cropFrom[1] + this.cropSize[1]
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.defaultBox
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.initialized) {
66
- this.api = $.Jcrop("#imageToCrop", this.jcropOptions)
67
- this.initialized = true
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.cropFromField.value = Math.round(coords.x) + "x" + Math.round(coords.y)
73
- this.cropFromField.dispatchEvent(new Event("change"))
74
- this.cropSizeField.value = Math.round(coords.w) + "x" + Math.round(coords.h)
75
- this.cropFromField.dispatchEvent(new Event("change"))
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.api.setSelect(this.defaultBox)
80
- this.cropFromField.value = `${this.box[0]}x${this.box[1]}`
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.api) {
86
- this.api.destroy()
102
+ if (this.#cropper) {
103
+ this.#cropper.destroy()
87
104
  }
88
- this.initialized = false
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 the GUI.
26
- Alchemy.GUI.init()
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 Alchemy.Dialog {
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 = HandlebarsTemplates.node_folder({ node: node })
17
+ leftIconArea.innerHTML = Handlebars.templates["node_folder.hbs"]({
18
+ node: node
19
+ })
18
20
  } else {
19
21
  leftIconArea.innerHTML = "&nbsp;"
20
22
  }