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.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -0
  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 +9 -1
  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/ingredients/link_view.rb +7 -1
  24. data/app/components/alchemy/ingredients/picture_view.rb +5 -2
  25. data/app/components/alchemy/ingredients/text_view.rb +4 -1
  26. data/app/components/concerns/alchemy/ingredients/link_target.rb +18 -0
  27. data/app/controllers/alchemy/admin/base_controller.rb +8 -3
  28. data/app/controllers/alchemy/admin/elements_controller.rb +2 -2
  29. data/app/controllers/alchemy/admin/layoutpages_controller.rb +1 -0
  30. data/app/controllers/alchemy/admin/pages_controller.rb +5 -1
  31. data/app/controllers/alchemy/elements_controller.rb +3 -0
  32. data/app/helpers/alchemy/admin/form_helper.rb +1 -1
  33. data/app/helpers/alchemy/admin/navigation_helper.rb +22 -1
  34. data/app/javascript/alchemy_admin/components/action.js +2 -1
  35. data/app/javascript/alchemy_admin/components/dialog_link.js +3 -18
  36. data/app/javascript/alchemy_admin/components/element_editor.js +9 -0
  37. data/app/javascript/alchemy_admin/components/elements_window.js +34 -0
  38. data/app/javascript/alchemy_admin/components/elements_window_handle.js +65 -0
  39. data/app/javascript/alchemy_admin/components/icon.js +2 -2
  40. data/app/javascript/alchemy_admin/components/index.js +1 -0
  41. data/app/javascript/alchemy_admin/components/preview_window.js +5 -5
  42. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +1 -1
  43. data/app/javascript/alchemy_admin/confirm_dialog.js +9 -11
  44. data/app/javascript/alchemy_admin/dialog.js +329 -0
  45. data/app/javascript/alchemy_admin/hotkeys.js +3 -2
  46. data/app/javascript/alchemy_admin/image_cropper.js +57 -40
  47. data/app/javascript/alchemy_admin/image_overlay.js +73 -0
  48. data/app/javascript/alchemy_admin/initializer.js +51 -2
  49. data/app/javascript/alchemy_admin/link_dialog.js +2 -1
  50. data/app/javascript/alchemy_admin/node_tree.js +3 -1
  51. data/app/javascript/alchemy_admin/page_sorter.js +1 -1
  52. data/app/javascript/alchemy_admin/picture_selector.js +2 -1
  53. data/app/javascript/alchemy_admin/shoelace_theme.js +2 -2
  54. data/app/javascript/alchemy_admin/templates/compiled.js +1 -0
  55. data/app/javascript/alchemy_admin.js +10 -6
  56. data/app/javascript/preview.js +117 -0
  57. data/app/models/alchemy/image_cropper_settings.rb +3 -4
  58. data/app/views/alchemy/_preview_mode_code.html.erb +1 -1
  59. data/app/views/alchemy/admin/crop.html.erb +19 -16
  60. data/app/views/alchemy/admin/dashboard/info.html.erb +1 -1
  61. data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +9 -8
  62. data/app/views/alchemy/admin/elements/_clipboard_button.html.erb +14 -0
  63. data/app/views/alchemy/admin/elements/_element.html.erb +2 -0
  64. data/app/views/alchemy/admin/elements/_form.html.erb +15 -13
  65. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +34 -0
  66. data/app/views/alchemy/admin/elements/index.html.erb +3 -15
  67. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +1 -1
  68. data/app/views/alchemy/admin/layoutpages/edit.html.erb +7 -5
  69. data/app/views/alchemy/admin/nodes/_form.html.erb +1 -1
  70. data/app/views/alchemy/admin/pages/_current_page.html.erb +1 -1
  71. data/app/views/alchemy/admin/pages/_form.html.erb +43 -40
  72. data/app/views/alchemy/admin/pages/_locked_page.html.erb +1 -1
  73. data/app/views/alchemy/admin/pages/_page_layout_filter.html.erb +1 -1
  74. data/app/views/alchemy/admin/pages/_sitemap.html.erb +1 -1
  75. data/app/views/alchemy/admin/pages/_table.html.erb +2 -2
  76. data/app/views/alchemy/admin/pages/edit.html.erb +1 -1
  77. data/app/views/alchemy/admin/pages/update.turbo_stream.erb +39 -0
  78. data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +3 -4
  79. data/app/views/alchemy/admin/pictures/_picture_description_field.html.erb +7 -5
  80. data/app/views/alchemy/admin/pictures/index.html.erb +13 -9
  81. data/app/views/alchemy/admin/resources/_filter_bar.html.erb +1 -1
  82. data/app/views/layouts/alchemy/admin.html.erb +8 -4
  83. data/bun.lockb +0 -0
  84. data/bundles/tinymce.js +2 -0
  85. data/config/alchemy/config.yml +3 -3
  86. data/config/alchemy/modules.yml +7 -6
  87. data/config/importmap.rb +4 -0
  88. data/config/routes.rb +1 -1
  89. data/lib/alchemy/engine.rb +6 -0
  90. data/lib/alchemy/modules.rb +0 -27
  91. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +10 -10
  92. data/lib/alchemy/tinymce.rb +2 -1
  93. data/lib/alchemy/upgrader/seven_point_four.rb +26 -0
  94. data/lib/alchemy/version.rb +1 -1
  95. data/lib/alchemy.rb +14 -0
  96. data/lib/alchemy_cms.rb +0 -2
  97. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +5 -0
  98. data/lib/generators/alchemy/ingredient/templates/view.html.erb +1 -1
  99. data/lib/generators/alchemy/ingredient/templates/view_component.rb.tt +10 -0
  100. data/lib/generators/alchemy/install/install_generator.rb +0 -1
  101. data/lib/generators/alchemy/install/templates/elements.yml.tt +1 -1
  102. data/lib/tasks/alchemy/upgrade.rake +19 -20
  103. data/rollup.config.mjs +44 -1
  104. data/vendor/javascript/cropperjs.min.js +10 -0
  105. data/vendor/javascript/handlebars.min.js +29 -0
  106. data/vendor/javascript/jquery.min.js +2 -0
  107. data/vendor/javascript/select2.min.js +23 -0
  108. data/vendor/javascript/tinymce.min.js +1 -1
  109. metadata +39 -91
  110. data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +0 -271
  111. data/app/assets/javascripts/alchemy/alchemy.image_overlay.coffee +0 -54
  112. data/app/assets/javascripts/alchemy/alchemy.preview.js.coffee +0 -97
  113. data/app/assets/javascripts/alchemy/preview.js +0 -1
  114. data/app/assets/javascripts/alchemy/templates/index.js +0 -2
  115. data/app/javascript/alchemy_admin/gui.js +0 -12
  116. data/app/views/alchemy/admin/elements/create.js.erb +0 -35
  117. data/app/views/alchemy/admin/pages/update.js.erb +0 -43
  118. data/lib/alchemy/upgrader/seven_point_zero.rb +0 -36
  119. data/vendor/assets/images/Jcrop.gif +0 -0
  120. data/vendor/assets/javascripts/jquery_plugins/jquery.Jcrop.min.js +0 -7
  121. data/vendor/assets/javascripts/jquery_plugins/select2.js +0 -3729
  122. data/vendor/assets/stylesheets/jquery.Jcrop.min.css +0 -2
  123. data/vendor/assets/stylesheets/tinymce/skins/content/default/content.min.css +0 -1
  124. /data/app/{assets/javascripts/alchemy → javascript/alchemy_admin}/templates/node_folder.hbs +0 -0
  125. /data/app/{assets/javascripts/alchemy → javascript/alchemy_admin}/templates/page_folder.hbs +0 -0
  126. /data/app/{assets/javascripts/tinymce/icons/remixicons/icons.js → javascript/tinymce/icons/remixicons/index.js} +0 -0
  127. /data/app/{assets/javascripts/tinymce/plugins/alchemy_link/plugin.min.js → javascript/tinymce/plugins/alchemy_link/index.js} +0 -0
  128. /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('meta[name="alchemy-icon-sprite"]')
10
- .getAttribute("content")
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 translate("Could not parse JSON result")
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
- 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