alchemy_cms 7.3.5 → 7.4.1

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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/Gemfile +3 -3
  4. data/README.md +2 -2
  5. data/alchemy_cms.gemspec +1 -4
  6. data/app/assets/builds/alchemy/admin.css +9 -1
  7. data/app/assets/builds/alchemy/admin.css.map +1 -1
  8. data/app/assets/builds/alchemy/custom-properties.css +1 -1
  9. data/app/assets/builds/alchemy/custom-properties.css.map +1 -1
  10. data/app/assets/builds/alchemy/preview.min.js +1 -0
  11. data/app/assets/builds/alchemy/welcome.css +1 -1
  12. data/app/assets/builds/alchemy/welcome.css.map +1 -1
  13. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  14. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css.map +1 -1
  15. data/app/assets/config/alchemy_manifest.js +0 -4
  16. data/app/assets/javascripts/alchemy/admin.js +8 -6
  17. data/app/assets/stylesheets/alchemy/admin/elements.scss +43 -7
  18. data/app/assets/stylesheets/alchemy/admin/forms.scss +4 -0
  19. data/app/assets/stylesheets/alchemy/admin/navigation.scss +9 -1
  20. data/app/assets/stylesheets/alchemy/admin/preview_window.scss +22 -17
  21. data/app/assets/stylesheets/alchemy/admin.scss +1 -1
  22. data/app/assets/stylesheets/alchemy/custom-properties.css +2 -1
  23. data/app/components/alchemy/ingredients/link_view.rb +7 -1
  24. data/app/components/alchemy/ingredients/picture_view.rb +5 -2
  25. data/app/components/alchemy/ingredients/text_view.rb +4 -1
  26. data/app/components/concerns/alchemy/ingredients/link_target.rb +18 -0
  27. data/app/controllers/alchemy/admin/base_controller.rb +8 -3
  28. data/app/controllers/alchemy/admin/elements_controller.rb +2 -2
  29. data/app/controllers/alchemy/admin/layoutpages_controller.rb +1 -0
  30. data/app/controllers/alchemy/admin/pages_controller.rb +5 -1
  31. data/app/controllers/alchemy/elements_controller.rb +3 -0
  32. data/app/helpers/alchemy/admin/form_helper.rb +1 -1
  33. data/app/helpers/alchemy/admin/navigation_helper.rb +22 -1
  34. data/app/javascript/alchemy_admin/components/action.js +2 -1
  35. data/app/javascript/alchemy_admin/components/dialog_link.js +3 -18
  36. data/app/javascript/alchemy_admin/components/element_editor.js +9 -0
  37. data/app/javascript/alchemy_admin/components/elements_window.js +34 -0
  38. data/app/javascript/alchemy_admin/components/elements_window_handle.js +65 -0
  39. data/app/javascript/alchemy_admin/components/icon.js +2 -2
  40. data/app/javascript/alchemy_admin/components/index.js +1 -0
  41. data/app/javascript/alchemy_admin/components/preview_window.js +5 -5
  42. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +1 -1
  43. data/app/javascript/alchemy_admin/confirm_dialog.js +9 -11
  44. data/app/javascript/alchemy_admin/dialog.js +329 -0
  45. data/app/javascript/alchemy_admin/hotkeys.js +3 -2
  46. data/app/javascript/alchemy_admin/image_cropper.js +56 -48
  47. data/app/javascript/alchemy_admin/image_overlay.js +73 -0
  48. data/app/javascript/alchemy_admin/initializer.js +51 -2
  49. data/app/javascript/alchemy_admin/link_dialog.js +2 -1
  50. data/app/javascript/alchemy_admin/node_tree.js +3 -1
  51. data/app/javascript/alchemy_admin/page_sorter.js +1 -1
  52. data/app/javascript/alchemy_admin/picture_selector.js +2 -1
  53. data/app/javascript/alchemy_admin/shoelace_theme.js +2 -2
  54. data/app/javascript/alchemy_admin/templates/compiled.js +1 -0
  55. data/app/javascript/alchemy_admin.js +10 -6
  56. data/app/javascript/preview.js +117 -0
  57. data/app/models/alchemy/image_cropper_settings.rb +3 -4
  58. data/app/views/alchemy/_preview_mode_code.html.erb +1 -1
  59. data/app/views/alchemy/admin/crop.html.erb +18 -16
  60. data/app/views/alchemy/admin/dashboard/info.html.erb +1 -1
  61. data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +9 -8
  62. data/app/views/alchemy/admin/elements/_clipboard_button.html.erb +14 -0
  63. data/app/views/alchemy/admin/elements/_element.html.erb +2 -0
  64. data/app/views/alchemy/admin/elements/_form.html.erb +15 -13
  65. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +34 -0
  66. data/app/views/alchemy/admin/elements/index.html.erb +3 -15
  67. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +1 -1
  68. data/app/views/alchemy/admin/layoutpages/edit.html.erb +7 -5
  69. data/app/views/alchemy/admin/nodes/_form.html.erb +1 -1
  70. data/app/views/alchemy/admin/pages/_current_page.html.erb +1 -1
  71. data/app/views/alchemy/admin/pages/_form.html.erb +43 -40
  72. data/app/views/alchemy/admin/pages/_locked_page.html.erb +1 -1
  73. data/app/views/alchemy/admin/pages/_page_layout_filter.html.erb +1 -1
  74. data/app/views/alchemy/admin/pages/_sitemap.html.erb +1 -1
  75. data/app/views/alchemy/admin/pages/_table.html.erb +2 -2
  76. data/app/views/alchemy/admin/pages/edit.html.erb +1 -1
  77. data/app/views/alchemy/admin/pages/update.turbo_stream.erb +39 -0
  78. data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +3 -4
  79. data/app/views/alchemy/admin/pictures/_picture_description_field.html.erb +7 -5
  80. data/app/views/alchemy/admin/pictures/index.html.erb +13 -9
  81. data/app/views/alchemy/admin/resources/_filter_bar.html.erb +1 -1
  82. data/app/views/layouts/alchemy/admin.html.erb +8 -4
  83. data/bun.lockb +0 -0
  84. data/bundles/tinymce.js +2 -0
  85. data/config/alchemy/config.yml +3 -3
  86. data/config/alchemy/modules.yml +7 -6
  87. data/config/importmap.rb +4 -0
  88. data/config/routes.rb +1 -1
  89. data/lib/alchemy/engine.rb +6 -0
  90. data/lib/alchemy/modules.rb +0 -27
  91. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +10 -10
  92. data/lib/alchemy/tinymce.rb +2 -1
  93. data/lib/alchemy/upgrader/seven_point_four.rb +26 -0
  94. data/lib/alchemy/version.rb +1 -1
  95. data/lib/alchemy.rb +14 -0
  96. data/lib/alchemy_cms.rb +0 -2
  97. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +5 -0
  98. data/lib/generators/alchemy/ingredient/templates/view.html.erb +1 -1
  99. data/lib/generators/alchemy/ingredient/templates/view_component.rb.tt +10 -0
  100. data/lib/generators/alchemy/install/install_generator.rb +0 -1
  101. data/lib/generators/alchemy/install/templates/elements.yml.tt +1 -1
  102. data/lib/tasks/alchemy/upgrade.rake +19 -20
  103. data/rollup.config.mjs +44 -1
  104. data/vendor/javascript/cropperjs.min.js +10 -0
  105. data/vendor/javascript/handlebars.min.js +29 -0
  106. data/vendor/javascript/jquery.min.js +2 -0
  107. data/vendor/javascript/select2.min.js +23 -0
  108. data/vendor/javascript/tinymce.min.js +1 -1
  109. metadata +40 -92
  110. data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +0 -271
  111. data/app/assets/javascripts/alchemy/alchemy.image_overlay.coffee +0 -54
  112. data/app/assets/javascripts/alchemy/alchemy.preview.js.coffee +0 -97
  113. data/app/assets/javascripts/alchemy/preview.js +0 -1
  114. data/app/assets/javascripts/alchemy/templates/index.js +0 -2
  115. data/app/javascript/alchemy_admin/gui.js +0 -12
  116. data/app/views/alchemy/admin/elements/create.js.erb +0 -35
  117. data/app/views/alchemy/admin/pages/update.js.erb +0 -43
  118. data/lib/alchemy/upgrader/seven_point_zero.rb +0 -36
  119. data/vendor/assets/images/Jcrop.gif +0 -0
  120. data/vendor/assets/javascripts/jquery_plugins/jquery.Jcrop.min.js +0 -7
  121. data/vendor/assets/javascripts/jquery_plugins/select2.js +0 -3729
  122. data/vendor/assets/stylesheets/jquery.Jcrop.min.css +0 -2
  123. data/vendor/assets/stylesheets/tinymce/skins/content/default/content.min.css +0 -1
  124. /data/app/{assets/javascripts/alchemy → javascript/alchemy_admin}/templates/node_folder.hbs +0 -0
  125. /data/app/{assets/javascripts/alchemy → javascript/alchemy_admin}/templates/page_folder.hbs +0 -0
  126. /data/app/{assets/javascripts/tinymce/icons/remixicons/icons.js → javascript/tinymce/icons/remixicons/index.js} +0 -0
  127. /data/app/{assets/javascripts/tinymce/plugins/alchemy_link/plugin.min.js → javascript/tinymce/plugins/alchemy_link/index.js} +0 -0
  128. /data/vendor/assets/{fonts → images}/remixicon.symbol.svg +0 -0
@@ -0,0 +1,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
@@ -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,22 @@
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[:default_box].to_json %>,
30
+ <%= @settings[:ratio] %>,
31
+ [
32
+ "<%= params[:crop_from_form_field_id] %>",
33
+ "<%= params[:crop_size_form_field_id] %>",
34
+ ],
35
+ <%= @element.id %>
36
+ );
35
37
  </script>
36
38
  <% 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
+ ) %>
@@ -4,6 +4,7 @@
4
4
  data-element-name="<%= element.name %>"
5
5
  class="<%= element.css_classes.join(" ") %>"
6
6
  <%= element.compact? ? "compact" : nil %>
7
+ <%= local_assigns[:created] ? "created" : nil %>
7
8
  <%= element.fixed? ? "fixed" : nil %>
8
9
  >
9
10
  <% unless element.fixed? %>
@@ -68,6 +69,7 @@
68
69
  <% if element.nestable_elements.any? %>
69
70
  <div class="nestable-elements">
70
71
  <%= content_tag :div,
72
+ id: "element_#{element.id}_nested_elements",
71
73
  class: "nested-elements", data: {
72
74
  'droppable-elements' => element.nestable_elements.join(' '),
73
75
  'element-name' => element.name
@@ -3,18 +3,20 @@
3
3
  <%= Alchemy.t(:no_more_elements_to_add) %>
4
4
  <% end %>
5
5
  <%- else -%>
6
- <%= alchemy_form_for [:admin, @element] do |form| %>
7
- <%= form.hidden_field :page_version_id %>
8
- <%= form.input :name,
9
- label: Alchemy.t(:element_of_type),
10
- collection: elements_for_select(@elements),
11
- prompt: Alchemy.t(:select_element),
12
- selected: (@elements.first if @elements.count == 1),
13
- input_html: {is: 'alchemy-select', autofocus: true} %>
14
- <% if @elements.count == 1 %>
15
- <%= form.hidden_field :name, value: @elements.first[:name] %>
16
- <% end %>
17
- <%= form.hidden_field :parent_element_id, value: @parent_element.try(:id) %>
18
- <%= form.submit Alchemy.t(:add) %>
6
+ <%= turbo_frame_tag @element do %>
7
+ <%= alchemy_form_for [:admin, @element], remote: false do |form| %>
8
+ <%= form.hidden_field :page_version_id %>
9
+ <%= form.input :name,
10
+ label: Alchemy.t(:element_of_type),
11
+ collection: elements_for_select(@elements),
12
+ prompt: Alchemy.t(:select_element),
13
+ selected: (@elements.first if @elements.count == 1),
14
+ input_html: {is: 'alchemy-select', autofocus: true} %>
15
+ <% if @elements.count == 1 %>
16
+ <%= form.hidden_field :name, value: @elements.first[:name] %>
17
+ <% end %>
18
+ <%= form.hidden_field :parent_element_id, value: @parent_element.try(:id) %>
19
+ <%= form.submit Alchemy.t(:add) %>
20
+ <%- end -%>
19
21
  <%- end -%>
20
22
  <%- end -%>
@@ -0,0 +1,34 @@
1
+ <% opts = {
2
+ partial: "alchemy/admin/elements/element",
3
+ locals: {
4
+ element: Alchemy::ElementEditor.new(@element),
5
+ created: true
6
+ }
7
+ } %>
8
+
9
+ <% if @element.fixed? %>
10
+ <% target = "fixed_element_#{@element.id}" %>
11
+ <% elsif @element.parent_element %>
12
+ <% target = "element_#{@element.parent_element_id}_nested_elements" %>
13
+ <% else %>
14
+ <% target = "main-content-elements" %>
15
+ <% end %>
16
+
17
+ <%- if @cut_element_id -%>
18
+ <%= turbo_stream.remove "element_#{@cut_element_id}" %>
19
+ <% end %>
20
+
21
+ <% if @insert_at_top %>
22
+ <%= turbo_stream.prepend target, **opts %>
23
+ <% else %>
24
+ <%= turbo_stream.append target, **opts %>
25
+ <% end %>
26
+
27
+ <%= turbo_stream.replace "clipboard_button",
28
+ partial: "alchemy/admin/elements/clipboard_button" %>
29
+
30
+ <alchemy-growl>
31
+ <%= Alchemy.t(:successfully_added_element) %>
32
+ </alchemy-growl>
33
+
34
+ <alchemy-action name="closeCurrentDialog"></alchemy-action>
@@ -1,4 +1,5 @@
1
1
  <%= turbo_frame_tag "alchemy_elements_window" do %>
2
+ <alchemy-elements-window-handle></alchemy-elements-window-handle>
2
3
  <alchemy-elements-window>
3
4
  <div class="elements-window-toolbar">
4
5
  <%= render Alchemy::Admin::ToolbarButton.new(
@@ -12,20 +13,7 @@
12
13
  },
13
14
  if_permitted_to: [:create, Alchemy::Element]
14
15
  ) %>
15
- <%= render Alchemy::Admin::ToolbarButton.new(
16
- url: alchemy.admin_clipboard_path(remarkable_type: "elements"),
17
- label: Alchemy.t("Show clipboard"),
18
- icon: :clipboard,
19
- icon_style: clipboard_empty?("elements") ? "line" : "fill",
20
- dialog_options: {
21
- title: Alchemy.t("Clipboard"),
22
- size: "400x305"
23
- },
24
- link_options: {
25
- id: "clipboard_button"
26
- },
27
- if_permitted_to: [:index, :alchemy_admin_clipboard]
28
- ) %>
16
+ <%= render "alchemy/admin/elements/clipboard_button" %>
29
17
  <sl-tooltip content="<%= Alchemy.t("Collapse all elements") %>" placement="top-end" class="right">
30
18
  <button id="collapse-all-elements-button" class="icon_button">
31
19
  <alchemy-icon name="contract-up-down"></alchemy-icon>
@@ -53,7 +41,7 @@
53
41
  <%= render @elements.map { |element| Alchemy::ElementEditor.new(element) } %>
54
42
  </sl-tab-panel>
55
43
  <% @fixed_elements.each do |element| %>
56
- <sl-tab-panel name="fixed-element-<%= element.id %>" style="--padding: 0" class="scrollable-elements">
44
+ <sl-tab-panel id="fixed_element_<%= element.id %>" name="fixed-element-<%= element.id %>" style="--padding: 0" class="scrollable-elements">
57
45
  <%= render Alchemy::ElementEditor.new(element) %>
58
46
  </sl-tab-panel>
59
47
  <% end %>
@@ -1,6 +1,6 @@
1
1
  <%= f.input :caption, as: ingredient.settings[:caption_as_textarea] ? 'text' : 'string' %>
2
2
  <%= f.input :title %>
3
- <%= f.input :alt_tag, as: :text, placeholder: ingredient.alt_text(language: @language) %>
3
+ <%= f.input :alt_tag, as: :text, placeholder: ingredient.alt_text(language: @language), input_html: {rows: 4} %>
4
4
  <%- if ingredient.settings[:sizes].present? && ingredient.settings[:srcset].blank? -%>
5
5
  <%= f.input :render_size,
6
6
  collection: [
@@ -1,7 +1,9 @@
1
- <%= alchemy_form_for [:admin, @page], url: alchemy.admin_layoutpage_path(@page), class: 'edit_page' do |f| %>
2
- <%= f.input :name, autofocus: true %>
3
- <%= render Alchemy::Admin::TagsAutocomplete.new do %>
4
- <%= f.input :tag_list, input_html: { value: f.object.tag_list.join(",") } %>
1
+ <%= turbo_frame_tag @page do %>
2
+ <%= alchemy_form_for [:admin, @page], url: alchemy.admin_layoutpage_path(@page), class: 'edit_page', remote: false do |f| %>
3
+ <%= f.input :name, autofocus: true %>
4
+ <%= render Alchemy::Admin::TagsAutocomplete.new do %>
5
+ <%= f.input :tag_list, input_html: { value: f.object.tag_list.join(",") } %>
6
+ <% end %>
7
+ <%= f.submit Alchemy.t(:save) %>
5
8
  <% end %>
6
- <%= f.submit Alchemy.t(:save) %>
7
9
  <% end %>