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
@@ -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
+ ) %>