alchemy_cms 7.3.5 → 7.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -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 +57 -40
  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 +19 -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 +39 -91
  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,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
+ ) %>
@@ -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 %>