alchemy_cms 7.3.4 → 7.4.0

Sign up to get free protection for your applications and to get access to all the features.
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
  }