alchemy_cms 7.3.4 → 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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -1
  3. data/Gemfile +10 -3
  4. data/README.md +2 -2
  5. data/Rakefile +2 -0
  6. data/alchemy_cms.gemspec +1 -4
  7. data/app/assets/builds/alchemy/admin.css +9 -1
  8. data/app/assets/builds/alchemy/admin.css.map +1 -1
  9. data/app/assets/builds/alchemy/custom-properties.css +1 -1
  10. data/app/assets/builds/alchemy/custom-properties.css.map +1 -1
  11. data/app/assets/builds/alchemy/preview.min.js +1 -0
  12. data/app/assets/builds/alchemy/welcome.css +1 -1
  13. data/app/assets/builds/alchemy/welcome.css.map +1 -1
  14. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  15. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css.map +1 -1
  16. data/app/assets/config/alchemy_manifest.js +0 -4
  17. data/app/assets/javascripts/alchemy/admin.js +8 -6
  18. data/app/assets/stylesheets/alchemy/admin/elements.scss +43 -7
  19. data/app/assets/stylesheets/alchemy/admin/forms.scss +4 -0
  20. data/app/assets/stylesheets/alchemy/admin/image_library.scss +40 -26
  21. data/app/assets/stylesheets/alchemy/admin/navigation.scss +9 -1
  22. data/app/assets/stylesheets/alchemy/admin/preview_window.scss +22 -17
  23. data/app/assets/stylesheets/alchemy/admin.scss +1 -1
  24. data/app/assets/stylesheets/alchemy/custom-properties.css +2 -1
  25. data/app/components/alchemy/ingredients/link_view.rb +7 -1
  26. data/app/components/alchemy/ingredients/picture_view.rb +5 -2
  27. data/app/components/alchemy/ingredients/text_view.rb +4 -1
  28. data/app/components/concerns/alchemy/ingredients/link_target.rb +18 -0
  29. data/app/controllers/alchemy/admin/base_controller.rb +34 -5
  30. data/app/controllers/alchemy/admin/elements_controller.rb +2 -2
  31. data/app/controllers/alchemy/admin/languages_controller.rb +1 -1
  32. data/app/controllers/alchemy/admin/layoutpages_controller.rb +1 -0
  33. data/app/controllers/alchemy/admin/pages_controller.rb +6 -6
  34. data/app/controllers/alchemy/admin/resources_controller.rb +1 -1
  35. data/app/controllers/alchemy/elements_controller.rb +3 -0
  36. data/app/helpers/alchemy/admin/form_helper.rb +1 -1
  37. data/app/helpers/alchemy/admin/navigation_helper.rb +22 -1
  38. data/app/javascript/alchemy_admin/components/action.js +2 -1
  39. data/app/javascript/alchemy_admin/components/dialog_link.js +3 -18
  40. data/app/javascript/alchemy_admin/components/element_editor.js +9 -0
  41. data/app/javascript/alchemy_admin/components/elements_window.js +34 -0
  42. data/app/javascript/alchemy_admin/components/elements_window_handle.js +65 -0
  43. data/app/javascript/alchemy_admin/components/icon.js +2 -2
  44. data/app/javascript/alchemy_admin/components/index.js +1 -0
  45. data/app/javascript/alchemy_admin/components/preview_window.js +5 -5
  46. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +1 -1
  47. data/app/javascript/alchemy_admin/confirm_dialog.js +9 -11
  48. data/app/javascript/alchemy_admin/dialog.js +329 -0
  49. data/app/javascript/alchemy_admin/hotkeys.js +3 -2
  50. data/app/javascript/alchemy_admin/image_cropper.js +57 -40
  51. data/app/javascript/alchemy_admin/image_overlay.js +73 -0
  52. data/app/javascript/alchemy_admin/initializer.js +51 -2
  53. data/app/javascript/alchemy_admin/link_dialog.js +2 -1
  54. data/app/javascript/alchemy_admin/node_tree.js +3 -1
  55. data/app/javascript/alchemy_admin/page_sorter.js +1 -1
  56. data/app/javascript/alchemy_admin/picture_selector.js +2 -1
  57. data/app/javascript/alchemy_admin/shoelace_theme.js +2 -2
  58. data/app/javascript/alchemy_admin/templates/compiled.js +1 -0
  59. data/app/javascript/alchemy_admin.js +10 -6
  60. data/app/javascript/preview.js +117 -0
  61. data/app/models/alchemy/image_cropper_settings.rb +3 -4
  62. data/app/views/alchemy/_menubar.html.erb +1 -1
  63. data/app/views/alchemy/_preview_mode_code.html.erb +1 -1
  64. data/app/views/alchemy/admin/crop.html.erb +19 -16
  65. data/app/views/alchemy/admin/dashboard/info.html.erb +1 -1
  66. data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +9 -8
  67. data/app/views/alchemy/admin/elements/_clipboard_button.html.erb +14 -0
  68. data/app/views/alchemy/admin/elements/_element.html.erb +2 -0
  69. data/app/views/alchemy/admin/elements/_form.html.erb +15 -13
  70. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +34 -0
  71. data/app/views/alchemy/admin/elements/index.html.erb +3 -15
  72. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +1 -1
  73. data/app/views/alchemy/admin/layoutpages/edit.html.erb +7 -5
  74. data/app/views/alchemy/admin/nodes/_form.html.erb +1 -1
  75. data/app/views/alchemy/admin/pages/_current_page.html.erb +1 -1
  76. data/app/views/alchemy/admin/pages/_form.html.erb +43 -40
  77. data/app/views/alchemy/admin/pages/_locked_page.html.erb +1 -1
  78. data/app/views/alchemy/admin/pages/_page_layout_filter.html.erb +1 -1
  79. data/app/views/alchemy/admin/pages/_sitemap.html.erb +1 -1
  80. data/app/views/alchemy/admin/pages/_table.html.erb +2 -2
  81. data/app/views/alchemy/admin/pages/edit.html.erb +1 -1
  82. data/app/views/alchemy/admin/pages/update.turbo_stream.erb +39 -0
  83. data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +3 -4
  84. data/app/views/alchemy/admin/pictures/_picture_description_field.html.erb +7 -5
  85. data/app/views/alchemy/admin/pictures/index.html.erb +13 -9
  86. data/app/views/alchemy/admin/resources/_filter_bar.html.erb +1 -1
  87. data/app/views/layouts/alchemy/admin.html.erb +8 -4
  88. data/bun.lockb +0 -0
  89. data/bundles/tinymce.js +2 -0
  90. data/config/alchemy/config.yml +3 -3
  91. data/config/alchemy/modules.yml +7 -6
  92. data/config/importmap.rb +4 -0
  93. data/config/routes.rb +1 -1
  94. data/lib/alchemy/engine.rb +6 -0
  95. data/lib/alchemy/modules.rb +0 -27
  96. data/lib/alchemy/resource.rb +14 -4
  97. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +10 -10
  98. data/lib/alchemy/tinymce.rb +2 -1
  99. data/lib/alchemy/upgrader/seven_point_four.rb +26 -0
  100. data/lib/alchemy/version.rb +1 -1
  101. data/lib/alchemy.rb +14 -0
  102. data/lib/alchemy_cms.rb +0 -2
  103. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +5 -0
  104. data/lib/generators/alchemy/ingredient/templates/view.html.erb +1 -1
  105. data/lib/generators/alchemy/ingredient/templates/view_component.rb.tt +10 -0
  106. data/lib/generators/alchemy/install/install_generator.rb +0 -1
  107. data/lib/generators/alchemy/install/templates/elements.yml.tt +1 -1
  108. data/lib/tasks/alchemy/upgrade.rake +19 -20
  109. data/rollup.config.mjs +44 -1
  110. data/vendor/javascript/cropperjs.min.js +10 -0
  111. data/vendor/javascript/handlebars.min.js +29 -0
  112. data/vendor/javascript/jquery.min.js +2 -0
  113. data/vendor/javascript/select2.min.js +23 -0
  114. data/vendor/javascript/tinymce.min.js +1 -1
  115. metadata +40 -94
  116. data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +0 -271
  117. data/app/assets/javascripts/alchemy/alchemy.image_overlay.coffee +0 -54
  118. data/app/assets/javascripts/alchemy/alchemy.preview.js.coffee +0 -97
  119. data/app/assets/javascripts/alchemy/preview.js +0 -1
  120. data/app/assets/javascripts/alchemy/templates/index.js +0 -2
  121. data/app/javascript/alchemy_admin/gui.js +0 -12
  122. data/app/views/alchemy/admin/elements/create.js.erb +0 -35
  123. data/app/views/alchemy/admin/pages/update.js.erb +0 -43
  124. data/lib/alchemy/upgrader/seven_point_zero.rb +0 -36
  125. data/vendor/assets/images/Jcrop.gif +0 -0
  126. data/vendor/assets/javascripts/jquery_plugins/jquery.Jcrop.min.js +0 -7
  127. data/vendor/assets/javascripts/jquery_plugins/select2.js +0 -3729
  128. data/vendor/assets/stylesheets/jquery.Jcrop.min.css +0 -2
  129. data/vendor/assets/stylesheets/tinymce/skins/content/default/content.min.css +0 -1
  130. /data/app/{assets/javascripts/alchemy → javascript/alchemy_admin}/templates/node_folder.hbs +0 -0
  131. /data/app/{assets/javascripts/alchemy → javascript/alchemy_admin}/templates/page_folder.hbs +0 -0
  132. /data/app/{assets/javascripts/tinymce/icons/remixicons/icons.js → javascript/tinymce/icons/remixicons/index.js} +0 -0
  133. /data/app/{assets/javascripts/tinymce/plugins/alchemy_link/plugin.min.js → javascript/tinymce/plugins/alchemy_link/index.js} +0 -0
  134. /data/vendor/assets/{fonts → images}/remixicon.symbol.svg +0 -0
@@ -11,6 +11,7 @@ module Alchemy
11
11
  # The Hash representing a Alchemy module
12
12
  #
13
13
  def alchemy_main_navigation_entry(alchemy_module)
14
+ validate_controller_existence!(alchemy_module)
14
15
  render(
15
16
  "alchemy/admin/partials/main_navigation_entry",
16
17
  alchemy_module: alchemy_module,
@@ -126,7 +127,7 @@ module Alchemy
126
127
  #
127
128
  def route_from_engine_or_main_app(engine_name, url_options)
128
129
  if engine_name.present?
129
- eval(engine_name).url_for(url_options) # rubocop:disable Security/Eval
130
+ send(engine_name).url_for(url_options)
130
131
  else
131
132
  main_app.url_for(url_options)
132
133
  end
@@ -141,6 +142,26 @@ module Alchemy
141
142
  url_options_for_navigation_entry(alchemy_module["navigation"] || {})
142
143
  end
143
144
 
145
+ # Validates the existence of a given controller configuration.
146
+ #
147
+ # @param String
148
+ # The controller name
149
+ def validate_controller_existence!(definition_hash)
150
+ controllers = [definition_hash["navigation"]["controller"]]
151
+
152
+ if definition_hash["navigation"]["sub_navigation"].is_a?(Array)
153
+ controllers.concat(definition_hash["navigation"]["sub_navigation"].map { |x| x["controller"] })
154
+ end
155
+
156
+ controllers.each do |controller|
157
+ controller_const_name = "#{controller.camelize}Controller"
158
+ controller_const_name.constantize
159
+ rescue NameError
160
+ raise "Error in AlchemyCMS module definition: '#{definition_hash["name"]}'. Could not find the " \
161
+ "matching controller class #{controller_const_name} for the specified controller: '#{controller}'"
162
+ end
163
+ end
164
+
144
165
  # Returns a url options hash for given navigation entry.
145
166
  #
146
167
  # @param [Hash]
@@ -1,4 +1,5 @@
1
1
  import { reloadPreview } from "alchemy_admin/components/preview_window"
2
+ import { closeCurrentDialog } from "alchemy_admin/dialog"
2
3
  import IngredientAnchorLink from "alchemy_admin/ingredient_anchor_link"
3
4
 
4
5
  class Action extends HTMLElement {
@@ -10,7 +11,7 @@ class Action extends HTMLElement {
10
11
  // add a intermediate closeCurrentDialog - action
11
12
  // this will be gone, if all dialogs are working with a promise and
12
13
  // we don't have to implicitly close the dialog
13
- closeCurrentDialog: Alchemy.closeCurrentDialog,
14
+ closeCurrentDialog,
14
15
  reloadPreview,
15
16
  updateAnchorIcon: IngredientAnchorLink.updateIcon
16
17
  }
@@ -1,13 +1,4 @@
1
- export const DEFAULTS = {
2
- header_height: 36,
3
- size: "400x300",
4
- padding: true,
5
- title: "",
6
- modal: true,
7
- overflow: "visible",
8
- ready: () => {},
9
- closed: () => {}
10
- }
1
+ import { Dialog } from "alchemy_admin/dialog"
11
2
 
12
3
  export class DialogLink extends HTMLAnchorElement {
13
4
  constructor() {
@@ -23,10 +14,7 @@ export class DialogLink extends HTMLAnchorElement {
23
14
  }
24
15
 
25
16
  openDialog() {
26
- this.dialog = new Alchemy.Dialog(
27
- this.getAttribute("href"),
28
- this.dialogOptions
29
- )
17
+ this.dialog = new Dialog(this.getAttribute("href"), this.dialogOptions)
30
18
  this.dialog.open()
31
19
  }
32
20
 
@@ -34,10 +22,7 @@ export class DialogLink extends HTMLAnchorElement {
34
22
  const options = this.dataset.dialogOptions
35
23
  ? JSON.parse(this.dataset.dialogOptions)
36
24
  : {}
37
- return {
38
- ...DEFAULTS,
39
- ...options
40
- }
25
+ return options
41
26
  }
42
27
 
43
28
  get disabled() {
@@ -40,6 +40,15 @@ export class ElementEditor extends HTMLElement {
40
40
  return
41
41
  }
42
42
 
43
+ // When newly created, focus the element and refresh the preview
44
+ if (this.hasAttribute("created")) {
45
+ this.focusElement()
46
+ this.previewWindow?.refresh().then(() => {
47
+ this.focusElementPreview()
48
+ })
49
+ this.removeAttribute("created")
50
+ }
51
+
43
52
  // Init GUI elements
44
53
  ImageLoader.init(this)
45
54
  fileEditors(
@@ -2,6 +2,7 @@ import SortableElements from "alchemy_admin/sortable_elements"
2
2
 
3
3
  class ElementsWindow extends HTMLElement {
4
4
  #visible = true
5
+ #turboFrame = null
5
6
 
6
7
  constructor() {
7
8
  super()
@@ -19,6 +20,7 @@ class ElementsWindow extends HTMLElement {
19
20
  ?.trigger("FocusElementEditor.Alchemy")
20
21
  }
21
22
  SortableElements()
23
+ this.resize()
22
24
  }
23
25
 
24
26
  collapseAllElements() {
@@ -38,10 +40,12 @@ class ElementsWindow extends HTMLElement {
38
40
  this.toggleButton
39
41
  .querySelector("alchemy-icon")
40
42
  .setAttribute("name", "menu-unfold")
43
+ this.resize()
41
44
  }
42
45
 
43
46
  hide() {
44
47
  document.body.classList.remove("elements-window-visible")
48
+ document.body.style.removeProperty("--elements-window-width")
45
49
  this.#visible = false
46
50
  this.toggleButton.closest("sl-tooltip").content = Alchemy.t("Show elements")
47
51
  this.toggleButton
@@ -49,6 +53,17 @@ class ElementsWindow extends HTMLElement {
49
53
  .setAttribute("name", "menu-fold")
50
54
  }
51
55
 
56
+ resize(width) {
57
+ if (width === undefined) {
58
+ width = this.widthFromCookie
59
+ }
60
+
61
+ if (width) {
62
+ document.body.style.setProperty("--elements-window-width", `${width}px`)
63
+ document.cookie = `alchemy-elements-window-width=${width}; SameSite=Lax; Path=/;`
64
+ }
65
+ }
66
+
52
67
  get collapseButton() {
53
68
  return this.querySelector("#collapse-all-elements-button")
54
69
  }
@@ -61,6 +76,25 @@ class ElementsWindow extends HTMLElement {
61
76
  return document.getElementById("alchemy_preview_window")
62
77
  }
63
78
 
79
+ get turboFrame() {
80
+ if (!this.#turboFrame) {
81
+ this.#turboFrame = this.closest("turbo-frame")
82
+ }
83
+ return this.#turboFrame
84
+ }
85
+
86
+ get widthFromCookie() {
87
+ return document.cookie
88
+ .split("; ")
89
+ .find((row) => row.startsWith("alchemy-elements-window-width="))
90
+ ?.split("=")[1]
91
+ }
92
+
93
+ set isDragged(dragged) {
94
+ this.turboFrame.style.transitionProperty = dragged ? "none" : null
95
+ this.turboFrame.style.pointerEvents = dragged ? "none" : null
96
+ }
97
+
64
98
  #attachEvents() {
65
99
  this.collapseButton?.addEventListener("click", () => {
66
100
  this.collapseAllElements()
@@ -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
  }