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
@@ -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
  }
@@ -44,7 +44,7 @@ export function displayPageFolders() {
44
44
  }
45
45
 
46
46
  if (list.children.length > 0 || page.folded) {
47
- pageFolderEl.outerHTML = HandlebarsTemplates.page_folder({ page })
47
+ pageFolderEl.outerHTML = Handlebars.templates["page_folder.hbs"]({ page })
48
48
  } else {
49
49
  pageFolderEl.innerHTML = ""
50
50
  }
@@ -1,4 +1,5 @@
1
1
  import { on } from "alchemy_admin/utils/events"
2
+ import { openDialog } from "alchemy_admin/dialog"
2
3
 
3
4
  function toggleCheckboxes(state) {
4
5
  document
@@ -58,7 +59,7 @@ export default function PictureSelector() {
58
59
 
59
60
  const url = editMultiplePicturesUrl(event.target.href)
60
61
 
61
- Alchemy.openDialog(url, {
62
+ openDialog(url, {
62
63
  title: event.target.title,
63
64
  size: "400x295"
64
65
  })
@@ -43,8 +43,8 @@ setDefaultAnimation("dialog.hide", {
43
43
  })
44
44
 
45
45
  const spriteUrl = document
46
- .querySelector('meta[name="alchemy-icon-sprite"]')
47
- .getAttribute("content")
46
+ .querySelector('link[rel="preload"][as="image"]')
47
+ .getAttribute("href")
48
48
 
49
49
  const iconMap = {
50
50
  "x-lg": "close"
@@ -0,0 +1 @@
1
+ (()=>{var n=Handlebars.template,e=Handlebars.templates=Handlebars.templates||{};e["node_folder.hbs"]=n({1:function(n,e,l,a,r){return"right"},3:function(n,e,l,a,r){return"down"},compiler:[8,">= 4.3.0"],main:function(n,e,l,a,r){var o,t=n.lambda,u=n.escapeExpression,c=n.lookupProperty||function(n,e){if(Object.prototype.hasOwnProperty.call(n,e))return n[e]};return'<a class="node_folder" data-record-id="'+u(t(null!=(o=null!=e?c(e,"node"):e)?c(o,"id"):o,e))+'" data-record-type="'+u(t(null!=(o=null!=e?c(e,"node"):e)?c(o,"type"):o,e))+'">\n <alchemy-icon name="arrow-'+(null!=(o=c(l,"if").call(null!=e?e:n.nullContext||{},null!=(o=null!=e?c(e,"node"):e)?c(o,"folded"):o,{name:"if",hash:{},fn:n.program(1,r,0),inverse:n.program(3,r,0),data:r,loc:{start:{line:2,column:28},end:{line:2,column:72}}}))?o:"")+'-s"></alchemy-icon>\n</a>\n'},useData:!0}),e["page_folder.hbs"]=n({1:function(n,e,l,a,r){return"right"},3:function(n,e,l,a,r){return"down"},compiler:[8,">= 4.3.0"],main:function(n,e,l,a,r){var o,t=n.lookupProperty||function(n,e){if(Object.prototype.hasOwnProperty.call(n,e))return n[e]};return'<a class="page_folder icon_button" data-page-id="'+n.escapeExpression(n.lambda(null!=(o=null!=e?t(e,"page"):e)?t(o,"id"):o,e))+'">\n <alchemy-icon name="arrow-'+(null!=(o=t(l,"if").call(null!=e?e:n.nullContext||{},null!=(o=null!=e?t(e,"page"):e)?t(o,"folded"):o,{name:"if",hash:{},fn:n.program(1,r,0),inverse:n.program(3,r,0),data:r,loc:{start:{line:2,column:28},end:{line:2,column:72}}}))?o:"")+'-s"></alchemy-icon>\n</a>\n'},useData:!0})})();
@@ -1,18 +1,20 @@
1
+ // We still use jQuery in some places (ie. select2)
2
+ import "handlebars"
3
+ import "jquery"
1
4
  import "@ungap/custom-elements"
2
5
  import "@hotwired/turbo-rails"
6
+ import "select2"
3
7
 
4
8
  import Rails from "@rails/ujs"
5
9
 
6
- import GUI from "alchemy_admin/gui"
7
10
  import { translate } from "alchemy_admin/i18n"
11
+ import { currentDialog, closeCurrentDialog } from "alchemy_admin/dialog"
8
12
  import Dirty from "alchemy_admin/dirty"
9
13
  import * as FixedElements from "alchemy_admin/fixed_elements"
10
14
  import { growl } from "alchemy_admin/growler"
11
15
  import ImageLoader from "alchemy_admin/image_loader"
12
- import ImageCropper from "alchemy_admin/image_cropper"
13
16
  import Initializer from "alchemy_admin/initializer"
14
17
  import { LinkDialog } from "alchemy_admin/link_dialog"
15
- import pictureSelector from "alchemy_admin/picture_selector"
16
18
  import pleaseWaitOverlay from "alchemy_admin/please_wait_overlay"
17
19
  import Sitemap from "alchemy_admin/sitemap"
18
20
  import Spinner from "alchemy_admin/spinner"
@@ -26,6 +28,9 @@ import {
26
28
  // Web Components
27
29
  import "alchemy_admin/components"
28
30
 
31
+ // Handlebars Templates
32
+ import "alchemy_admin/templates/compiled"
33
+
29
34
  // Shoelace Setup
30
35
  import "alchemy_admin/shoelace_theme"
31
36
 
@@ -36,15 +41,14 @@ if (typeof window.Alchemy === "undefined") {
36
41
 
37
42
  // Enhance the global Alchemy object with imported features
38
43
  Object.assign(Alchemy, {
44
+ closeCurrentDialog,
45
+ currentDialog,
39
46
  ...Dirty,
40
- GUI,
41
47
  t: translate, // Global utility method for translating a given string
42
48
  FixedElements,
43
49
  growl,
44
50
  ImageLoader: ImageLoader.init,
45
- ImageCropper,
46
51
  LinkDialog,
47
- pictureSelector,
48
52
  pleaseWaitOverlay,
49
53
  Sitemap,
50
54
  Spinner,
@@ -0,0 +1,117 @@
1
+ window.Alchemy = Alchemy || {}
2
+
3
+ Object.assign(Alchemy, {
4
+ ElementSelector: {
5
+ styles: {
6
+ reset: {
7
+ outline: "",
8
+ "outline-offset": "",
9
+ cursor: ""
10
+ },
11
+ hover: {
12
+ outline: "2px dashed #f0b437",
13
+ "outline-offset": "4px",
14
+ cursor: "pointer"
15
+ },
16
+ selected: {
17
+ outline: "2px dashed #90b9d0",
18
+ "outline-offset": "4px"
19
+ }
20
+ },
21
+
22
+ init() {
23
+ window.addEventListener("message", (event) => {
24
+ switch (event.data.message) {
25
+ case "Alchemy.blurElements":
26
+ this.blurElements()
27
+ break
28
+ case "Alchemy.focusElement":
29
+ this.focusElement(event.data)
30
+ break
31
+ default:
32
+ console.info("Received unknown message!", event.data)
33
+ }
34
+ })
35
+ this.elements = Array.from(
36
+ document.querySelectorAll("[data-alchemy-element]")
37
+ )
38
+ this.elements.forEach((element) => {
39
+ element.addEventListener("mouseover", () => {
40
+ if (!element.classList.contains("selected")) {
41
+ Object.assign(element.style, this.getStyle("hover"))
42
+ }
43
+ })
44
+ element.addEventListener("mouseout", () => {
45
+ if (!element.classList.contains("selected")) {
46
+ Object.assign(element.style, this.getStyle("reset"))
47
+ }
48
+ })
49
+ element.addEventListener("click", (e) => {
50
+ e.stopPropagation()
51
+ e.preventDefault()
52
+ this.selectElement(element)
53
+ this.focusElementEditor(element)
54
+ })
55
+ })
56
+ },
57
+
58
+ // Mark element in preview frame as selected and scrolls to it.
59
+ selectElement(element) {
60
+ this.blurElements(element)
61
+ element.classList.add("selected")
62
+ Object.assign(element.style, this.getStyle("selected"))
63
+ element.scrollIntoView({
64
+ behavior: "smooth",
65
+ block: "start"
66
+ })
67
+ },
68
+
69
+ // Blur all elements in preview frame.
70
+ blurElements(selectedElement) {
71
+ this.elements.forEach((element) => {
72
+ if (element !== selectedElement) {
73
+ element.classList.remove("selected")
74
+ Object.assign(element.style, this.getStyle("reset"))
75
+ }
76
+ })
77
+ },
78
+
79
+ // Focus the element in the Alchemy preview window.
80
+ focusElement(data) {
81
+ const element = this.getElement(data.element_id)
82
+ if (element) {
83
+ return this.selectElement(element)
84
+ } else {
85
+ return console.warn("Could not focus element with id", data.element_id)
86
+ }
87
+ },
88
+
89
+ getElement(element_id) {
90
+ return this.elements.find(
91
+ (element) => element.dataset.alchemyElement === element_id.toString()
92
+ )
93
+ },
94
+
95
+ // Focus the element editor in the Alchemy element window.
96
+ focusElementEditor(element) {
97
+ const element_id = element.dataset.alchemyElement
98
+ window.parent.postMessage(
99
+ {
100
+ message: "Alchemy.focusElementEditor",
101
+ element_id
102
+ },
103
+ window.location.origin
104
+ )
105
+ },
106
+
107
+ getStyle(state) {
108
+ if (state === "reset") {
109
+ return this.styles["reset"]
110
+ } else {
111
+ return this.styles[state]
112
+ }
113
+ }
114
+ }
115
+ })
116
+
117
+ Alchemy.ElementSelector.init()
@@ -20,8 +20,7 @@ module Alchemy
20
20
  {
21
21
  min_size: large_enough? ? min_size : false,
22
22
  ratio: ratio,
23
- default_box: default_box,
24
- image_size: [image_width, image_height]
23
+ default_box: default_box
25
24
  }.freeze
26
25
  end
27
26
 
@@ -79,8 +78,8 @@ module Alchemy
79
78
  [
80
79
  default_crop_from[0],
81
80
  default_crop_from[1],
82
- default_crop_from[0] + default_crop_size[0],
83
- default_crop_from[1] + default_crop_size[1]
81
+ default_crop_size[0],
82
+ default_crop_size[1]
84
83
  ]
85
84
  end
86
85
  end
@@ -111,7 +111,7 @@
111
111
  </div>
112
112
  </template>
113
113
 
114
- <script type="module">
114
+ <script type="module" data-turbo-eval="false">
115
115
  class Menubar extends HTMLElement {
116
116
  constructor() {
117
117
  super()
@@ -2,5 +2,5 @@
2
2
  <script type="text/javascript">
3
3
  Alchemy = { locale: "<%= session[:alchemy_locale] %>" };
4
4
  </script>
5
- <%= javascript_include_tag("alchemy/preview") %>
5
+ <%= javascript_include_tag("alchemy/preview.min") %>
6
6
  <% end %>
@@ -8,7 +8,7 @@
8
8
  <%= simple_format Alchemy.t(:explain_cropping) %>
9
9
  <% end %>
10
10
  <div class="thumbnail_background">
11
- <%= image_tag @picture.thumbnail_url(size: '800x600'), id: 'imageToCrop' %>
11
+ <%= image_tag @picture.url(flatten: true), id: 'imageToCrop' %>
12
12
  </div>
13
13
  <form>
14
14
  <%= button_tag Alchemy.t(:apply), type: 'submit' %>
@@ -17,20 +17,23 @@
17
17
  </div>
18
18
  <% end %>
19
19
  <% if @settings %>
20
- <script type="text/javascript">
21
- Alchemy.ImageLoader('#jscropper .thumbnail_background');
22
- $('#imageToCrop').on("load", function() {
23
- new Alchemy.ImageCropper(
24
- <%= @settings[:min_size].to_json %>,
25
- <%= @settings[:default_box].to_json %>,
26
- <%= @settings[:ratio] %>,
27
- <%= @settings[:image_size].to_json %>,
28
- [
29
- "<%= params[:crop_from_form_field_id] %>",
30
- "<%= params[:crop_size_form_field_id] %>",
31
- ],
32
- <%= @element.id %>
33
- );
34
- });
20
+ <script type="module">
21
+ import ImageCropper from "alchemy_admin/image_cropper";
22
+ import ImageLoader from "alchemy_admin/image_loader";
23
+
24
+ const image = document.getElementById("imageToCrop");
25
+
26
+ new ImageLoader(image);
27
+ new ImageCropper(
28
+ image,
29
+ <%= @settings[:min_size].to_json %>,
30
+ <%= @settings[:default_box].to_json %>,
31
+ <%= @settings[:ratio] %>,
32
+ [
33
+ "<%= params[:crop_from_form_field_id] %>",
34
+ "<%= params[:crop_size_form_field_id] %>",
35
+ ],
36
+ <%= @element.id %>
37
+ );
35
38
  </script>
36
39
  <% end %>
@@ -22,7 +22,7 @@
22
22
  <%= Alchemy.t 'Update status unavailable' %>
23
23
  </span>
24
24
  </p>
25
- <script type="text/javascript">
25
+ <script type="module">
26
26
  var el = $('#update_check');
27
27
  var spinner = new Alchemy.Spinner('small')
28
28
  spinner.spin(el[0])
@@ -3,14 +3,15 @@
3
3
  (nestable_element = element.nestable_elements.first) &&
4
4
  Alchemy::Element.all_from_clipboard_for_parent_element(get_clipboard("elements"), element).none?
5
5
  %>
6
- <%= form_for [:admin, Alchemy::Element.new(name: nestable_element)],
7
- remote: true, html: { class: 'add-nested-element-form', id: nil } do |f| %>
8
- <%= f.hidden_field :name %>
9
- <%= f.hidden_field :page_version_id, value: element.page_version_id %>
10
- <%= f.hidden_field :parent_element_id, value: element.id %>
11
- <button class="add-nestable-element-button" is="alchemy-button" data-turbo="false">
12
- <%= Alchemy.t(:add_nested_element, name: Alchemy.t(nestable_element.to_sym, scope: 'element_names')) %>
13
- </button>
6
+ <%= turbo_frame_tag("new_nested_element_#{element.id}") do %>
7
+ <%= form_for [:admin, Alchemy::Element.new(name: nestable_element)], html: { class: 'add-nested-element-form', id: nil } do |f| %>
8
+ <%= f.hidden_field :name %>
9
+ <%= f.hidden_field :page_version_id, value: element.page_version_id %>
10
+ <%= f.hidden_field :parent_element_id, value: element.id %>
11
+ <button class="add-nestable-element-button" is="alchemy-button">
12
+ <%= Alchemy.t(:add_nested_element, name: Alchemy.t(nestable_element.to_sym, scope: 'element_names')) %>
13
+ </button>
14
+ <% end %>
14
15
  <% end %>
15
16
  <% else %>
16
17
  <%= link_to_dialog (nestable_element ? Alchemy.t(:add_nested_element, name: Alchemy.t(nestable_element.to_sym, scope: 'element_names')) : Alchemy.t("New Element")),
@@ -0,0 +1,14 @@
1
+ <%= render Alchemy::Admin::ToolbarButton.new(
2
+ url: alchemy.admin_clipboard_path(remarkable_type: "elements"),
3
+ label: Alchemy.t("Show clipboard"),
4
+ icon: :clipboard,
5
+ icon_style: clipboard_empty?("elements") ? "line" : "fill",
6
+ dialog_options: {
7
+ title: Alchemy.t("Clipboard"),
8
+ size: "400x305"
9
+ },
10
+ link_options: {
11
+ id: "clipboard_button"
12
+ },
13
+ if_permitted_to: [:index, :alchemy_admin_clipboard]
14
+ ) %>