alchemy_cms 7.1.7 → 7.2.0.b

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 (305) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +133 -0
  3. data/Gemfile +2 -4
  4. data/LICENSE +1 -1
  5. data/README.md +5 -6
  6. data/SECURITY.md +1 -1
  7. data/alchemy_cms.gemspec +3 -4
  8. data/app/assets/javascripts/alchemy/admin.js +0 -9
  9. data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +5 -15
  10. data/app/assets/javascripts/alchemy/alchemy.image_overlay.coffee +5 -4
  11. data/app/assets/javascripts/alchemy/templates/index.js +0 -1
  12. data/app/assets/javascripts/alchemy/templates/node_folder.hbs +1 -1
  13. data/app/assets/javascripts/alchemy/templates/page_folder.hbs +1 -1
  14. data/app/assets/javascripts/tinymce/plugins/alchemy_link/plugin.min.js +20 -7
  15. data/app/assets/stylesheets/alchemy/_custom-properties.scss +12 -0
  16. data/app/assets/stylesheets/alchemy/_mixins.scss +10 -6
  17. data/app/assets/stylesheets/alchemy/_variables.scss +3 -0
  18. data/app/assets/stylesheets/alchemy/admin.scss +2 -2
  19. data/app/assets/stylesheets/alchemy/archive.scss +4 -3
  20. data/app/assets/stylesheets/alchemy/attachment-select.scss +19 -0
  21. data/app/assets/stylesheets/alchemy/base.scss +31 -18
  22. data/app/assets/stylesheets/alchemy/buttons.scss +3 -4
  23. data/app/assets/stylesheets/alchemy/dashboard.scss +1 -1
  24. data/app/assets/stylesheets/alchemy/dialogs.scss +2 -5
  25. data/app/assets/stylesheets/alchemy/elements.scss +76 -46
  26. data/app/assets/stylesheets/alchemy/flash.scss +20 -70
  27. data/app/assets/stylesheets/alchemy/forms.scss +41 -36
  28. data/app/assets/stylesheets/alchemy/frame.scss +12 -3
  29. data/app/assets/stylesheets/alchemy/icons.scss +34 -2
  30. data/app/assets/stylesheets/alchemy/image_library.scss +18 -9
  31. data/app/assets/stylesheets/alchemy/{filter_field.scss → list_filter.scss} +8 -7
  32. data/app/assets/stylesheets/alchemy/lists.scss +1 -1
  33. data/app/assets/stylesheets/alchemy/navigation.scss +9 -12
  34. data/app/assets/stylesheets/alchemy/node-select.scss +1 -1
  35. data/app/assets/stylesheets/alchemy/nodes.scss +15 -13
  36. data/app/assets/stylesheets/alchemy/notices.scss +56 -39
  37. data/app/assets/stylesheets/alchemy/page-select.scss +1 -4
  38. data/app/assets/stylesheets/alchemy/pagination.scss +11 -1
  39. data/app/assets/stylesheets/alchemy/preview_window.scss +7 -3
  40. data/app/assets/stylesheets/alchemy/search.scss +4 -4
  41. data/app/assets/stylesheets/alchemy/selects.scss +13 -7
  42. data/app/assets/stylesheets/alchemy/shoelace.scss +33 -2
  43. data/app/assets/stylesheets/alchemy/sitemap.scss +155 -159
  44. data/app/assets/stylesheets/alchemy/tables.scss +49 -12
  45. data/app/assets/stylesheets/alchemy/tags.scss +17 -11
  46. data/app/assets/stylesheets/alchemy/toolbar.scss +2 -2
  47. data/app/assets/stylesheets/alchemy/typography.scss +41 -22
  48. data/app/assets/stylesheets/alchemy/upload.scss +5 -4
  49. data/app/components/alchemy/admin/attachment_select.rb +39 -0
  50. data/app/components/alchemy/admin/icon.rb +72 -0
  51. data/app/components/alchemy/admin/link_dialog/anchor_tab.rb +41 -0
  52. data/app/components/alchemy/admin/link_dialog/base_tab.rb +75 -0
  53. data/app/components/alchemy/admin/link_dialog/external_tab.rb +42 -0
  54. data/app/components/alchemy/admin/link_dialog/file_tab.rb +45 -0
  55. data/app/components/alchemy/admin/link_dialog/internal_tab.rb +66 -0
  56. data/app/components/alchemy/admin/link_dialog/tabs.rb +33 -0
  57. data/app/components/alchemy/admin/list_filter.rb +42 -0
  58. data/app/components/alchemy/admin/message.rb +19 -0
  59. data/app/components/alchemy/admin/tags_autocomplete.rb +25 -0
  60. data/app/components/alchemy/admin/toolbar_button.rb +111 -0
  61. data/app/components/alchemy/ingredients/link_view.rb +1 -7
  62. data/app/components/alchemy/ingredients/picture_view.rb +2 -2
  63. data/app/components/alchemy/ingredients/text_view.rb +1 -2
  64. data/app/controllers/alchemy/admin/base_controller.rb +1 -1
  65. data/app/controllers/alchemy/admin/elements_controller.rb +4 -2
  66. data/app/controllers/alchemy/admin/ingredients_controller.rb +2 -0
  67. data/app/controllers/alchemy/admin/languages_controller.rb +1 -1
  68. data/app/controllers/alchemy/admin/legacy_page_urls_controller.rb +12 -4
  69. data/app/controllers/alchemy/admin/nodes_controller.rb +26 -0
  70. data/app/controllers/alchemy/admin/pages_controller.rb +11 -78
  71. data/app/controllers/alchemy/admin/picture_descriptions_controller.rb +15 -0
  72. data/app/controllers/alchemy/admin/pictures_controller.rb +18 -1
  73. data/app/controllers/alchemy/admin/resources_controller.rb +15 -10
  74. data/app/controllers/alchemy/api/attachments_controller.rb +44 -0
  75. data/app/controllers/alchemy/api/pages_controller.rb +10 -6
  76. data/app/controllers/alchemy/base_controller.rb +2 -2
  77. data/app/controllers/alchemy/messages_controller.rb +2 -2
  78. data/app/controllers/alchemy/pages_controller.rb +8 -6
  79. data/app/controllers/concerns/alchemy/admin/current_language.rb +1 -1
  80. data/app/controllers/concerns/alchemy/legacy_page_redirects.rb +1 -1
  81. data/app/decorators/alchemy/element_editor.rb +2 -2
  82. data/app/helpers/alchemy/admin/base_helper.rb +8 -60
  83. data/app/helpers/alchemy/admin/elements_helper.rb +1 -1
  84. data/app/helpers/alchemy/admin/ingredients_helper.rb +1 -1
  85. data/app/helpers/alchemy/base_helper.rb +9 -91
  86. data/app/helpers/alchemy/elements_helper.rb +3 -3
  87. data/app/helpers/alchemy/pages_helper.rb +16 -9
  88. data/app/javascript/alchemy_admin/components/attachment_select.js +24 -0
  89. data/app/javascript/alchemy_admin/components/button.js +3 -0
  90. data/app/javascript/alchemy_admin/components/clipboard_button.js +3 -2
  91. data/app/javascript/alchemy_admin/components/dialog_link.js +10 -7
  92. data/app/javascript/alchemy_admin/components/dom_id_select.js +69 -0
  93. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +42 -0
  94. data/app/javascript/alchemy_admin/components/element_editor/publish_element_button.js +4 -2
  95. data/app/javascript/alchemy_admin/components/element_editor.js +21 -13
  96. data/app/javascript/alchemy_admin/components/elements_window.js +87 -0
  97. data/app/javascript/alchemy_admin/components/growl.js +13 -0
  98. data/app/javascript/alchemy_admin/components/icon.js +51 -0
  99. data/app/javascript/alchemy_admin/components/index.js +24 -0
  100. data/app/javascript/alchemy_admin/components/ingredient_group.js +6 -0
  101. data/app/javascript/alchemy_admin/components/link_buttons/link_button.js +21 -11
  102. data/app/javascript/alchemy_admin/components/link_buttons/unlink_button.js +2 -1
  103. data/app/javascript/alchemy_admin/components/link_buttons.js +1 -0
  104. data/app/javascript/alchemy_admin/components/list_filter.js +68 -0
  105. data/app/javascript/alchemy_admin/components/message.js +69 -0
  106. data/app/javascript/alchemy_admin/components/node_select.js +1 -1
  107. data/app/javascript/alchemy_admin/components/overlay.js +6 -6
  108. data/app/javascript/alchemy_admin/components/page_select.js +3 -7
  109. data/app/javascript/alchemy_admin/components/preview_window.js +121 -0
  110. data/app/javascript/alchemy_admin/components/remote_select.js +4 -1
  111. data/app/javascript/alchemy_admin/components/select.js +37 -1
  112. data/app/javascript/alchemy_admin/components/tags_autocomplete.js +57 -0
  113. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +4 -3
  114. data/app/javascript/alchemy_admin/components/uploader/progress.js +1 -1
  115. data/app/javascript/alchemy_admin/confirm_dialog.js +133 -0
  116. data/app/javascript/alchemy_admin/dirty.js +19 -14
  117. data/app/javascript/alchemy_admin/fixed_elements.js +24 -0
  118. data/app/javascript/alchemy_admin/growler.js +15 -0
  119. data/app/javascript/alchemy_admin/gui.js +2 -4
  120. data/app/javascript/alchemy_admin/hotkeys.js +60 -0
  121. data/app/javascript/alchemy_admin/image_loader.js +2 -2
  122. data/app/javascript/alchemy_admin/ingredient_anchor_link.js +2 -3
  123. data/app/javascript/alchemy_admin/initializer.js +1 -8
  124. data/app/javascript/alchemy_admin/link_dialog.js +131 -0
  125. data/app/javascript/alchemy_admin/locales/en.js +3 -0
  126. data/app/javascript/alchemy_admin/node_tree.js +4 -3
  127. data/app/javascript/alchemy_admin/page_sorter.js +23 -14
  128. data/app/javascript/alchemy_admin/picture_editors.js +3 -2
  129. data/app/javascript/alchemy_admin/shoelace_theme.js +60 -0
  130. data/app/javascript/alchemy_admin/sitemap.js +9 -3
  131. data/app/javascript/alchemy_admin/sortable_elements.js +4 -6
  132. data/app/javascript/alchemy_admin.js +18 -42
  133. data/app/models/alchemy/current.rb +26 -0
  134. data/app/models/alchemy/element.rb +1 -1
  135. data/app/models/alchemy/ingredients/headline.rb +8 -1
  136. data/app/models/alchemy/ingredients/picture.rb +6 -0
  137. data/app/models/alchemy/language.rb +8 -6
  138. data/app/models/alchemy/node.rb +2 -2
  139. data/app/models/alchemy/page/page_elements.rb +8 -8
  140. data/app/models/alchemy/page/page_layouts.rb +3 -3
  141. data/app/models/alchemy/page/page_natures.rb +13 -9
  142. data/app/models/alchemy/page/page_scopes.rb +2 -2
  143. data/app/models/alchemy/page/publisher.rb +1 -0
  144. data/app/models/alchemy/page.rb +13 -28
  145. data/app/models/alchemy/picture.rb +8 -0
  146. data/app/models/alchemy/picture_description.rb +8 -0
  147. data/app/models/alchemy/picture_variant.rb +1 -1
  148. data/app/models/alchemy/site.rb +10 -7
  149. data/app/serializers/alchemy/attachment_serializer.rb +8 -0
  150. data/app/serializers/alchemy/page_node_serializer.rb +9 -0
  151. data/app/views/alchemy/_menubar.html.erb +1 -1
  152. data/app/views/alchemy/_preview_mode_code.html.erb +1 -1
  153. data/app/views/alchemy/admin/attachments/_tag_list.html.erb +2 -2
  154. data/app/views/alchemy/admin/attachments/archive_overlay.js.erb +0 -1
  155. data/app/views/alchemy/admin/attachments/edit.html.erb +3 -4
  156. data/app/views/alchemy/admin/clipboard/clear.js.erb +1 -1
  157. data/app/views/alchemy/admin/clipboard/index.html.erb +1 -1
  158. data/app/views/alchemy/admin/clipboard/insert.js.erb +1 -1
  159. data/app/views/alchemy/admin/clipboard/remove.js.erb +1 -1
  160. data/app/views/alchemy/admin/dashboard/_locked_pages.html.erb +1 -1
  161. data/app/views/alchemy/admin/dashboard/_sites.html.erb +1 -1
  162. data/app/views/alchemy/admin/dashboard/help.html.erb +48 -12
  163. data/app/views/alchemy/admin/dashboard/index.html.erb +1 -1
  164. data/app/views/alchemy/admin/dashboard/info.html.erb +5 -8
  165. data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +1 -1
  166. data/app/views/alchemy/admin/elements/_element.html.erb +5 -5
  167. data/app/views/alchemy/admin/elements/_footer.html.erb +1 -1
  168. data/app/views/alchemy/admin/elements/_header.html.erb +6 -2
  169. data/app/views/alchemy/admin/elements/_toolbar.html.erb +8 -6
  170. data/app/views/alchemy/admin/elements/create.js.erb +0 -5
  171. data/app/views/alchemy/admin/elements/index.html.erb +70 -34
  172. data/app/views/alchemy/admin/ingredients/_file_fields.html.erb +1 -2
  173. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +3 -5
  174. data/app/views/alchemy/admin/languages/_language.html.erb +1 -1
  175. data/app/views/alchemy/admin/languages/index.html.erb +2 -2
  176. data/app/views/alchemy/admin/layoutpages/_layoutpage.html.erb +18 -18
  177. data/app/views/alchemy/admin/layoutpages/edit.html.erb +3 -4
  178. data/app/views/alchemy/admin/layoutpages/index.html.erb +2 -2
  179. data/app/views/alchemy/admin/legacy_page_urls/_legacy_page_url.html.erb +10 -11
  180. data/app/views/alchemy/admin/legacy_page_urls/_new.html.erb +15 -17
  181. data/app/views/alchemy/admin/legacy_page_urls/_table.html.erb +16 -0
  182. data/app/views/alchemy/admin/legacy_page_urls/_update.turbo_stream.erb +12 -0
  183. data/app/views/alchemy/admin/legacy_page_urls/create.turbo_stream.erb +8 -0
  184. data/app/views/alchemy/admin/legacy_page_urls/destroy.turbo_stream.erb +1 -0
  185. data/app/views/alchemy/admin/legacy_page_urls/edit.html.erb +27 -0
  186. data/app/views/alchemy/admin/legacy_page_urls/show.html.erb +1 -0
  187. data/app/views/alchemy/admin/legacy_page_urls/update.turbo_stream.erb +1 -0
  188. data/app/views/alchemy/admin/nodes/_form.html.erb +12 -11
  189. data/app/views/alchemy/admin/nodes/_label.html.erb +1 -0
  190. data/app/views/alchemy/admin/nodes/_node.html.erb +19 -19
  191. data/app/views/alchemy/admin/nodes/_page_nodes.html.erb +48 -0
  192. data/app/views/alchemy/admin/nodes/_update.turbo_stream.erb +9 -0
  193. data/app/views/alchemy/admin/nodes/create.turbo_stream.erb +1 -0
  194. data/app/views/alchemy/admin/nodes/destroy.turbo_stream.erb +1 -0
  195. data/app/views/alchemy/admin/nodes/index.html.erb +3 -3
  196. data/app/views/alchemy/admin/pages/_form.html.erb +3 -4
  197. data/app/views/alchemy/admin/pages/_legacy_urls.html.erb +4 -15
  198. data/app/views/alchemy/admin/pages/_page.html.erb +39 -39
  199. data/app/views/alchemy/admin/pages/_table_row.html.erb +3 -3
  200. data/app/views/alchemy/admin/pages/_toolbar.html.erb +2 -2
  201. data/app/views/alchemy/admin/pages/configure.html.erb +6 -0
  202. data/app/views/alchemy/admin/pages/edit.html.erb +15 -62
  203. data/app/views/alchemy/admin/pages/unlock.js.erb +2 -2
  204. data/app/views/alchemy/admin/partials/_autocomplete_tag_list.html.erb +3 -1
  205. data/app/views/alchemy/admin/partials/_flash_notices.html.erb +4 -2
  206. data/app/views/alchemy/admin/partials/_language_tree_select.html.erb +1 -1
  207. data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +5 -2
  208. data/app/views/alchemy/admin/partials/_remote_search_form.html.erb +2 -2
  209. data/app/views/alchemy/admin/partials/_search_form.html.erb +2 -2
  210. data/app/views/alchemy/admin/partials/_site_select.html.erb +1 -1
  211. data/app/views/alchemy/admin/picture_descriptions/_form.html.erb +11 -0
  212. data/app/views/alchemy/admin/picture_descriptions/edit.html.erb +6 -0
  213. data/app/views/alchemy/admin/pictures/_form.html.erb +4 -3
  214. data/app/views/alchemy/admin/pictures/_infos.html.erb +1 -1
  215. data/app/views/alchemy/admin/pictures/_picture_description_field.html.erb +29 -0
  216. data/app/views/alchemy/admin/pictures/_tag_list.html.erb +2 -2
  217. data/app/views/alchemy/admin/pictures/archive_overlay.js.erb +0 -2
  218. data/app/views/alchemy/admin/pictures/edit_multiple.html.erb +3 -3
  219. data/app/views/alchemy/admin/pictures/show.html.erb +3 -3
  220. data/app/views/alchemy/admin/resources/_form.html.erb +3 -4
  221. data/app/views/alchemy/admin/resources/_tag_list.html.erb +2 -2
  222. data/app/views/alchemy/admin/resources/index.html.erb +2 -2
  223. data/app/views/alchemy/admin/sites/index.html.erb +1 -1
  224. data/app/views/alchemy/admin/styleguide/index.html.erb +29 -24
  225. data/app/views/alchemy/admin/tags/_tag.html.erb +1 -1
  226. data/app/views/alchemy/admin/tags/edit.html.erb +1 -1
  227. data/app/views/alchemy/admin/tags/index.html.erb +1 -1
  228. data/app/views/alchemy/base/500.html.erb +7 -18
  229. data/app/views/alchemy/base/error_notice.html.erb +3 -1
  230. data/app/views/alchemy/ingredients/_boolean_editor.html.erb +1 -1
  231. data/app/views/alchemy/ingredients/_datetime_editor.html.erb +1 -1
  232. data/app/views/alchemy/ingredients/_headline_editor.html.erb +13 -8
  233. data/app/views/alchemy/ingredients/_picture_editor.html.erb +1 -1
  234. data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +1 -1
  235. data/app/views/alchemy/language_links/_language.html.erb +1 -1
  236. data/app/views/kaminari/alchemy/_first_page.html.erb +2 -2
  237. data/app/views/kaminari/alchemy/_gap.html.erb +1 -1
  238. data/app/views/kaminari/alchemy/_last_page.html.erb +2 -2
  239. data/app/views/kaminari/alchemy/_next_page.html.erb +2 -2
  240. data/app/views/kaminari/alchemy/_prev_page.html.erb +2 -2
  241. data/app/views/layouts/alchemy/admin.html.erb +2 -1
  242. data/bundles/shoelace.js +3 -1
  243. data/config/locales/alchemy.en.yml +16 -3
  244. data/config/routes.rb +3 -1
  245. data/db/migrate/20240314105244_create_alchemy_picture_descriptions.rb +11 -0
  246. data/lib/alchemy/configuration_methods.rb +1 -1
  247. data/lib/alchemy/controller_actions.rb +3 -3
  248. data/lib/alchemy/element_definition.rb +10 -6
  249. data/lib/alchemy/engine.rb +19 -2
  250. data/lib/alchemy/page_layout.rb +10 -6
  251. data/lib/alchemy/permissions.rb +3 -2
  252. data/lib/alchemy/seeder.rb +2 -2
  253. data/lib/alchemy/test_support/capybara_helpers.rb +4 -0
  254. data/lib/alchemy/test_support/factories/language_factory.rb +1 -1
  255. data/lib/alchemy/test_support/shared_contexts.rb +8 -0
  256. data/lib/alchemy/tinymce.rb +2 -1
  257. data/lib/alchemy/version.rb +1 -1
  258. data/lib/alchemy.rb +36 -0
  259. data/lib/alchemy_cms.rb +0 -1
  260. data/lib/generators/alchemy/menus/templates/node.html.erb +2 -2
  261. data/lib/generators/alchemy/menus/templates/node.html.haml +2 -2
  262. data/lib/generators/alchemy/menus/templates/node.html.slim +2 -2
  263. data/lib/generators/alchemy/menus/templates/wrapper.html.erb +1 -1
  264. data/lib/generators/alchemy/menus/templates/wrapper.html.haml +1 -1
  265. data/lib/generators/alchemy/menus/templates/wrapper.html.slim +1 -1
  266. data/lib/tasks/alchemy/sitemap.rake +97 -0
  267. data/package.json +8 -8
  268. data/vendor/assets/fonts/remixicon.symbol.svg +11 -0
  269. data/vendor/javascript/shoelace.min.js +333 -118
  270. data/vendor/javascript/sortable.min.js +1 -1
  271. data/vendor/javascript/tinymce.min.js +1 -1
  272. data/vendor/javascript/ungap-custom-elements.min.js +1 -1
  273. metadata +61 -54
  274. data/app/assets/javascripts/alchemy/alchemy.confirm_dialog.js.coffee +0 -85
  275. data/app/assets/javascripts/alchemy/alchemy.elements_window.js.coffee +0 -107
  276. data/app/assets/javascripts/alchemy/alchemy.file_progress.js.coffee +0 -66
  277. data/app/assets/javascripts/alchemy/alchemy.fixed_elements.js +0 -45
  278. data/app/assets/javascripts/alchemy/alchemy.growler.js.coffee +0 -24
  279. data/app/assets/javascripts/alchemy/alchemy.hotkeys.js.coffee +0 -49
  280. data/app/assets/javascripts/alchemy/alchemy.initializer.js.coffee +0 -0
  281. data/app/assets/javascripts/alchemy/alchemy.link_dialog.js.coffee +0 -230
  282. data/app/assets/javascripts/alchemy/alchemy.list_filter.js.coffee +0 -49
  283. data/app/assets/javascripts/alchemy/alchemy.preview_window.js.coffee +0 -82
  284. data/app/assets/javascripts/alchemy/alchemy.string_extension.js.coffee +0 -11
  285. data/app/assets/javascripts/alchemy/templates/page.hbs +0 -19
  286. data/app/javascript/alchemy_admin/tags_autocomplete.js +0 -46
  287. data/app/models/alchemy/tree_node.rb +0 -7
  288. data/app/views/alchemy/admin/elements/destroy.js.erb +0 -8
  289. data/app/views/alchemy/admin/legacy_page_urls/_form.html.erb +0 -5
  290. data/app/views/alchemy/admin/legacy_page_urls/create.js.erb +0 -9
  291. data/app/views/alchemy/admin/legacy_page_urls/destroy.js.erb +0 -6
  292. data/app/views/alchemy/admin/legacy_page_urls/update.js.erb +0 -2
  293. data/app/views/alchemy/admin/pages/_anchor_link.html.erb +0 -22
  294. data/app/views/alchemy/admin/pages/_external_link.html.erb +0 -31
  295. data/app/views/alchemy/admin/pages/_file_link.html.erb +0 -31
  296. data/app/views/alchemy/admin/pages/_internal_link.html.erb +0 -35
  297. data/app/views/alchemy/admin/pages/link.html.erb +0 -26
  298. data/app/views/alchemy/admin/partials/_flash.html.erb +0 -4
  299. data/app/views/alchemy/admin/partials/_toolbar_button.html.erb +0 -29
  300. data/vendor/assets/fonts/remixicon.eot +0 -0
  301. data/vendor/assets/fonts/remixicon.svg +0 -7816
  302. data/vendor/assets/fonts/remixicon.ttf +0 -0
  303. data/vendor/assets/fonts/remixicon.woff +0 -0
  304. data/vendor/assets/fonts/remixicon.woff2 +0 -0
  305. data/vendor/assets/stylesheets/remixicon.scss +0 -10480
@@ -4,9 +4,11 @@ module Alchemy
4
4
  module BaseHelper
5
5
  # An alias for truncate.
6
6
  # Left here for downwards compatibilty.
7
+ # @deprecated
7
8
  def shorten(text, length)
8
9
  text.truncate(length: length)
9
10
  end
11
+ deprecate :shorten, deprecator: Alchemy::Deprecation
10
12
 
11
13
  # Logs a message in the Rails logger (warn level)
12
14
  # and optionally displays an error message to the user.
@@ -22,21 +24,12 @@ module Alchemy
22
24
  # Render a Remix icon
23
25
  #
24
26
  # @param icon_name [String] icon name
25
- # @option options - style: nil [String] icon style. line or fill
27
+ # @option options - style: fill [String] icon style. line or fill. Pass false for no style.
26
28
  # @option options - size: nil [String] icon size
27
29
  #
28
30
  # @return [String]
29
31
  def render_icon(icon_name, options = {})
30
- options = {style: "line", fixed_width: true}.merge(options)
31
- style = options[:style] && "-#{ri_style(options[:style])}"
32
- classes = [
33
- "icon",
34
- "ri-#{ri_icon(icon_name)}#{style}",
35
- options[:size] ? "ri-#{options[:size]}" : nil,
36
- options[:fixed_width] ? "ri-fw" : nil,
37
- options[:class]
38
- ].compact
39
- content_tag("i", nil, class: classes)
32
+ render Alchemy::Admin::Icon.new(icon_name, options)
40
33
  end
41
34
 
42
35
  # Returns a div with an icon and the passed content
@@ -50,34 +43,21 @@ module Alchemy
50
43
  # <% end %>
51
44
  #
52
45
  def render_message(type = :info, msg = nil, &blk)
53
- icon_class = message_icon_class(type)
54
- if blk
55
- content_tag :div, render_icon(icon_class) + capture(&blk), class: "#{type} message"
56
- else
57
- content_tag :div, render_icon(icon_class) + msg, class: "#{type} message"
58
- end
59
- end
60
-
61
- # Renders the flash partial (+alchemy/admin/partials/flash+)
62
- #
63
- # @param [String] notice The notice you want to display
64
- # @param [Symbol] style The style of this flash. Valid values are +:notice+ (default), +:warn+ and +:error+
65
- #
66
- def render_flash_notice(notice, style = :notice)
67
- render("alchemy/admin/partials/flash", flash_type: style, message: notice)
46
+ render Alchemy::Admin::Message.new(msg || capture(&blk), type: type)
68
47
  end
69
48
 
70
49
  # Checks if the given argument is a String or a Page object.
71
50
  # If a String is given, it tries to find the page via page_layout
72
51
  # Logs a warning if no page is given.
52
+ # @deprecated
73
53
  def page_or_find(page)
74
- unless Language.current
54
+ unless Current.language
75
55
  warning("No default language set up")
76
56
  return nil
77
57
  end
78
58
 
79
59
  if page.is_a?(String)
80
- page = Language.current.pages.find_by(page_layout: page)
60
+ page = Current.language.pages.find_by(page_layout: page)
81
61
  end
82
62
  if page.blank?
83
63
  warning("No Page found for #{page.inspect}")
@@ -86,68 +66,6 @@ module Alchemy
86
66
  page
87
67
  end
88
68
  end
89
-
90
- # Returns the icon name for given message type
91
- #
92
- # @param message_type [String] The message type. One of +warning+, +info+, +notice+, +error+
93
- # @return [String] The icon name
94
- def message_icon_class(message_type)
95
- case message_type.to_s
96
- when "warning", "warn", "alert" then "exclamation"
97
- when "notice" then "check"
98
- when "error" then "bug"
99
- when "hint" then "info"
100
- else
101
- message_type
102
- end
103
- end
104
-
105
- private
106
-
107
- # Returns the Remix icon name for given icon name
108
- #
109
- # @param icon_name [String] The icon name.
110
- # @return [String] The Remix icon class
111
- def ri_icon(icon_name)
112
- case icon_name.to_s
113
- when "minus", "remove", "delete"
114
- "delete-bin-2"
115
- when "plus"
116
- "add"
117
- when "copy"
118
- "file-copy"
119
- when "download"
120
- "download-2"
121
- when "upload"
122
- "upload-2"
123
- when "exclamation"
124
- "alert"
125
- when "info-circle", "info"
126
- "information"
127
- when "times"
128
- "close"
129
- when "tag"
130
- "price-tag-3"
131
- when "cog"
132
- "settings-3"
133
- else
134
- icon_name
135
- end
136
- end
137
-
138
- # Returns the Remix icon style for given style
139
- #
140
- # @param style [String] The style name
141
- # @return [String] The RemixIcon style
142
- def ri_style(style)
143
- case style.to_s
144
- when "solid", "fill"
145
- "fill"
146
- when "line", "regular"
147
- "line"
148
- else
149
- style
150
- end
151
- end
69
+ deprecate :page_or_find, deprecator: Alchemy::Deprecation
152
70
  end
153
71
  end
@@ -72,13 +72,13 @@ module Alchemy
72
72
  #
73
73
  def render_elements(options = {}, &blk)
74
74
  options = {
75
- from_page: @page,
75
+ from_page: Current.page,
76
76
  render_format: "html"
77
77
  }.update(options)
78
78
 
79
79
  finder = options[:finder] || Alchemy::ElementsFinder.new(options)
80
80
 
81
- page_version = if @preview_mode
81
+ page_version = if Current.preview_page?
82
82
  options[:from_page]&.draft_version
83
83
  else
84
84
  options[:from_page]&.public_version
@@ -173,7 +173,7 @@ module Alchemy
173
173
 
174
174
  # Returns a hash containing the HTML tag attributes required for preview mode.
175
175
  def element_preview_code_attributes(element)
176
- return {} unless element.present? && @preview_mode && element.page == @page
176
+ return {} unless element.present? && Current.preview_page?(element.page)
177
177
 
178
178
  {"data-alchemy-element" => element.id}
179
179
  end
@@ -64,9 +64,8 @@ module Alchemy
64
64
  #
65
65
  def render_site_layout(&block)
66
66
  render current_alchemy_site, &block
67
- rescue ActionView::MissingTemplate
68
- warning("Site layout for #{current_alchemy_site.try(:name)} not found. Please run `rails g alchemy:site_layouts`")
69
- ""
67
+ rescue ActionView::MissingTemplate => error
68
+ error_or_warning(error, "Site layout for #{current_alchemy_site.try(:name)} not found. Please run `rails g alchemy:site_layouts`")
70
69
  end
71
70
 
72
71
  # Renders a menu partial
@@ -79,7 +78,7 @@ module Alchemy
79
78
  def render_menu(menu_type, options = {})
80
79
  root_node = Alchemy::Node.roots.find_by(
81
80
  menu_type: menu_type,
82
- language: Alchemy::Language.current
81
+ language: Alchemy::Current.language
83
82
  )
84
83
  if root_node.nil?
85
84
  warning("Menu with type #{menu_type} not found!")
@@ -87,11 +86,8 @@ module Alchemy
87
86
  end
88
87
 
89
88
  render("alchemy/menus/#{menu_type}/wrapper", menu: root_node, options: options)
90
- rescue ActionView::MissingTemplate => e
91
- warning <<~WARN
92
- Menu partial not found for #{menu_type}.
93
- #{e}
94
- WARN
89
+ rescue ActionView::MissingTemplate => error
90
+ error_or_warning(error, "Menu partial for #{menu_type} not found. Please run `rails g alchemy:menus`")
95
91
  end
96
92
 
97
93
  # Returns page links in a breadcrumb beginning from root to current page.
@@ -169,5 +165,16 @@ module Alchemy
169
165
  def meta_robots
170
166
  "#{@page.robot_index? ? "" : "no"}index, #{@page.robot_follow? ? "" : "no"}follow"
171
167
  end
168
+
169
+ private
170
+
171
+ def error_or_warning(error, message)
172
+ if Rails.application.config.consider_all_requests_local?
173
+ raise error, message
174
+ else
175
+ Rails.logger.error message
176
+ ""
177
+ end
178
+ end
172
179
  end
173
180
  end
@@ -0,0 +1,24 @@
1
+ import { RemoteSelect } from "alchemy_admin/components/remote_select"
2
+
3
+ class AttachmentSelect extends RemoteSelect {
4
+ _renderResult(item) {
5
+ return this._renderListEntry(item)
6
+ }
7
+
8
+ /**
9
+ * html template for each list entry
10
+ * @param {object} page
11
+ * @returns {string}
12
+ * @private
13
+ */
14
+ _renderListEntry(attachment) {
15
+ return `
16
+ <div class="attachment-select--attachment">
17
+ <alchemy-icon name="${attachment.icon_css_class}"></alchemy-icon>
18
+ <span class="attachment-select--attachment-name">${attachment.name}</span>
19
+ </div>
20
+ `
21
+ }
22
+ }
23
+
24
+ customElements.define("alchemy-attachment-select", AttachmentSelect)
@@ -8,6 +8,8 @@ class Button extends HTMLButtonElement {
8
8
  if (this.form.dataset.remote == "true") {
9
9
  this.form.addEventListener("ajax:complete", this)
10
10
  }
11
+
12
+ this.form.addEventListener("turbo:submit-end", this)
11
13
  } else {
12
14
  console.warn("No form for button found!", this)
13
15
  }
@@ -26,6 +28,7 @@ class Button extends HTMLButtonElement {
26
28
  }
27
29
  break
28
30
  case "ajax:complete":
31
+ case "turbo:submit-end":
29
32
  this.enable()
30
33
  break
31
34
  }
@@ -1,11 +1,12 @@
1
1
  import "clipboard"
2
+ import { growl } from "alchemy_admin/growler"
2
3
 
3
4
  class ClipboardButton extends HTMLElement {
4
5
  constructor() {
5
6
  super()
6
7
 
7
8
  this.innerHTML = `
8
- <i class="icon ri-clipboard-line ri-fw"></i>
9
+ <alchemy-icon name="clipboard"></alchemy-icon>
9
10
  `
10
11
 
11
12
  this.clipboard = new ClipboardJS(this, {
@@ -15,7 +16,7 @@ class ClipboardButton extends HTMLElement {
15
16
  })
16
17
 
17
18
  this.clipboard.on("success", () => {
18
- Alchemy.growl(this.getAttribute("success-text"))
19
+ growl(this.getAttribute("success-text"))
19
20
  })
20
21
  }
21
22
 
@@ -10,13 +10,16 @@ export const DEFAULTS = {
10
10
  }
11
11
 
12
12
  export class DialogLink extends HTMLAnchorElement {
13
- connectedCallback() {
14
- this.addEventListener("click", (evt) => {
15
- if (!this.disabled) {
16
- this.openDialog()
17
- }
18
- evt.preventDefault()
19
- })
13
+ constructor() {
14
+ super()
15
+ this.addEventListener("click", this)
16
+ }
17
+
18
+ handleEvent(evt) {
19
+ if (!this.disabled) {
20
+ this.openDialog()
21
+ }
22
+ evt.preventDefault()
20
23
  }
21
24
 
22
25
  openDialog() {
@@ -0,0 +1,69 @@
1
+ import { get } from "alchemy_admin/utils/ajax"
2
+ import { translate } from "alchemy_admin/i18n"
3
+
4
+ class DomIdSelect extends HTMLElement {
5
+ dataItem(hash) {
6
+ return {
7
+ id: `#${hash}`,
8
+ text: `#${hash}`
9
+ }
10
+ }
11
+
12
+ get selectElement() {
13
+ return this.querySelector('select[is="alchemy-select"]')
14
+ }
15
+ }
16
+
17
+ class DomIdApiSelect extends DomIdSelect {
18
+ #pageId = undefined
19
+
20
+ connectedCallback() {
21
+ this.page = this.getAttribute("page")
22
+ }
23
+
24
+ async #fetchDomIds() {
25
+ const result = await get(Alchemy.routes.api_ingredients_path, {
26
+ page_id: this.#pageId
27
+ })
28
+ const options = result.data.ingredients
29
+ .filter((ingredient) => ingredient.data?.dom_id)
30
+ .map((ingredient) => this.dataItem(ingredient.data.dom_id))
31
+ const prompt =
32
+ options.length > 0 ? translate("None") : translate("No anchors found")
33
+
34
+ this.selectElement.setOptions(options, prompt)
35
+ this.selectElement.enable()
36
+ }
37
+
38
+ #reset() {
39
+ // wait a tick to initialize the alchemy-select
40
+ requestAnimationFrame(() => {
41
+ this.selectElement.disable()
42
+ this.selectElement.setOptions([], translate("Select a page first"))
43
+ })
44
+ }
45
+
46
+ set page(pageId) {
47
+ this.#pageId = pageId
48
+ pageId ? this.#fetchDomIds() : this.#reset()
49
+ }
50
+ }
51
+
52
+ class DomIdPreviewSelect extends DomIdSelect {
53
+ connectedCallback() {
54
+ // wait a tick to let the browser initialize the inner select component
55
+ requestAnimationFrame(() => {
56
+ const frame = document.getElementById("alchemy_preview_window")
57
+ const elements = frame.contentDocument?.querySelectorAll("[id]") || []
58
+ if (elements.length > 0) {
59
+ const options = Array.from(elements).map((element) => {
60
+ return this.dataItem(element.id)
61
+ })
62
+ this.selectElement.setOptions(options, translate("None"))
63
+ }
64
+ })
65
+ }
66
+ }
67
+
68
+ customElements.define("alchemy-dom-id-api-select", DomIdApiSelect)
69
+ customElements.define("alchemy-dom-id-preview-select", DomIdPreviewSelect)
@@ -0,0 +1,42 @@
1
+ import { removeTab } from "alchemy_admin/fixed_elements"
2
+ import { growl } from "alchemy_admin/growler"
3
+ import { reloadPreview } from "alchemy_admin/components/preview_window"
4
+ import { confirmToDeleteDialog } from "alchemy_admin/confirm_dialog"
5
+
6
+ export class DeleteElementButton extends HTMLElement {
7
+ constructor() {
8
+ super()
9
+ this.addEventListener("click", this)
10
+ }
11
+
12
+ handleEvent() {
13
+ confirmToDeleteDialog(this.url, { message: this.message }).then(
14
+ (response) => {
15
+ this.#removeElement(response)
16
+ }
17
+ )
18
+ }
19
+
20
+ #removeElement(response) {
21
+ const elementEditor = this.closest("alchemy-element-editor")
22
+ elementEditor.addEventListener("transitionend", () => {
23
+ if (elementEditor.fixed) {
24
+ removeTab(elementEditor.elementId)
25
+ }
26
+ elementEditor.remove()
27
+ })
28
+ elementEditor.classList.add("dismiss")
29
+ growl(response.message)
30
+ reloadPreview()
31
+ }
32
+
33
+ get url() {
34
+ return this.getAttribute("href")
35
+ }
36
+
37
+ get message() {
38
+ return this.getAttribute("message")
39
+ }
40
+ }
41
+
42
+ customElements.define("alchemy-delete-element-button", DeleteElementButton)
@@ -1,4 +1,6 @@
1
1
  import { patch } from "alchemy_admin/utils/ajax"
2
+ import { reloadPreview } from "alchemy_admin/components/preview_window"
3
+ import { growl } from "alchemy_admin/growler"
2
4
 
3
5
  export class PublishElementButton extends HTMLElement {
4
6
  constructor() {
@@ -14,9 +16,9 @@ export class PublishElementButton extends HTMLElement {
14
16
  .then((response) => {
15
17
  this.elementEditor.published = response.data.public
16
18
  this.tooltip.setAttribute("content", response.data.label)
17
- Alchemy.reloadPreview()
19
+ reloadPreview()
18
20
  })
19
- .catch((error) => Alchemy.growl(error.message, "error"))
21
+ .catch((error) => growl(error.message, "error"))
20
22
  }
21
23
  }
22
24
 
@@ -1,12 +1,14 @@
1
- import TagsAutocomplete from "alchemy_admin/tags_autocomplete"
2
1
  import ImageLoader from "alchemy_admin/image_loader"
3
2
  import fileEditors from "alchemy_admin/file_editors"
4
3
  import pictureEditors from "alchemy_admin/picture_editors"
4
+ import SortableElements from "alchemy_admin/sortable_elements"
5
5
  import IngredientAnchorLink from "alchemy_admin/ingredient_anchor_link"
6
6
  import { post } from "alchemy_admin/utils/ajax"
7
7
  import { createHtmlElement } from "alchemy_admin/utils/dom_helpers"
8
+ import { growl } from "alchemy_admin/growler"
8
9
 
9
10
  import "alchemy_admin/components/element_editor/publish_element_button"
11
+ import "alchemy_admin/components/element_editor/delete_element_button"
10
12
 
11
13
  export class ElementEditor extends HTMLElement {
12
14
  constructor() {
@@ -44,7 +46,7 @@ export class ElementEditor extends HTMLElement {
44
46
  `#${this.id} .ingredient-editor.file, #${this.id} .ingredient-editor.audio, #${this.id} .ingredient-editor.video`
45
47
  )
46
48
  pictureEditors(`#${this.id} .ingredient-editor.picture`)
47
- TagsAutocomplete(this)
49
+ SortableElements(`#${this.id} .nested-elements`)
48
50
  }
49
51
 
50
52
  handleEvent(event) {
@@ -99,7 +101,7 @@ export class ElementEditor extends HTMLElement {
99
101
  }
100
102
 
101
103
  focusElementPreview() {
102
- Alchemy.PreviewWindow.postMessage({
104
+ this.previewWindow?.postMessage({
103
105
  message: "Alchemy.focusElement",
104
106
  element_id: this.elementId
105
107
  })
@@ -139,11 +141,13 @@ export class ElementEditor extends HTMLElement {
139
141
  )
140
142
  })
141
143
  // Show message
142
- Alchemy.growl(warning, "warn")
144
+ growl(warning, "warn")
143
145
  this.elementErrors.classList.remove("hidden")
144
146
  } else {
145
- Alchemy.growl(data.notice)
146
- Alchemy.PreviewWindow.refresh(() => this.focusElementPreview())
147
+ growl(data.notice)
148
+ this.previewWindow?.refresh().then(() => {
149
+ this.focusElementPreview()
150
+ })
147
151
  this.updateTitle(data.previewText)
148
152
  data.ingredientAnchors.forEach((anchor) => {
149
153
  IngredientAnchorLink.updateIcon(anchor.ingredientId, anchor.active)
@@ -273,7 +277,7 @@ export class ElementEditor extends HTMLElement {
273
277
  }
274
278
  })
275
279
  .catch((error) => {
276
- Alchemy.growl(error.message, "error")
280
+ growl(error.message, "error")
277
281
  console.error(error)
278
282
  })
279
283
  .finally(() => {
@@ -320,7 +324,7 @@ export class ElementEditor extends HTMLElement {
320
324
  resolve()
321
325
  })
322
326
  .catch((error) => {
323
- Alchemy.growl(error.message, "error")
327
+ growl(error.message, "error")
324
328
  console.error(error)
325
329
  reject(error)
326
330
  })
@@ -353,9 +357,9 @@ export class ElementEditor extends HTMLElement {
353
357
  */
354
358
  set published(isPublished) {
355
359
  if (isPublished) {
356
- this.classList.remove("hidden")
360
+ this.classList.remove("element-hidden")
357
361
  } else {
358
- this.classList.add("hidden")
362
+ this.classList.add("element-hidden")
359
363
  }
360
364
  }
361
365
 
@@ -387,8 +391,8 @@ export class ElementEditor extends HTMLElement {
387
391
  set collapsed(value) {
388
392
  this.classList.toggle("folded", value)
389
393
  this.classList.toggle("expanded", !value)
390
- this.toggleIcon?.classList?.toggle("ri-arrow-down-s-line", !value)
391
- this.toggleIcon?.classList?.toggle("ri-arrow-left-s-line", value)
394
+ this.toggleIcon &&
395
+ (this.toggleIcon.name = value ? "arrow-left-s" : "arrow-down-s")
392
396
  }
393
397
 
394
398
  /**
@@ -475,7 +479,7 @@ export class ElementEditor extends HTMLElement {
475
479
  * @returns {HTMLElement|undefined}
476
480
  */
477
481
  get toggleIcon() {
478
- return this.toggleButton?.querySelector(".icon")
482
+ return this.toggleButton?.querySelector("alchemy-icon")
479
483
  }
480
484
 
481
485
  /**
@@ -549,6 +553,10 @@ export class ElementEditor extends HTMLElement {
549
553
  get parentElementEditor() {
550
554
  return this.parentElement?.closest("alchemy-element-editor")
551
555
  }
556
+
557
+ get previewWindow() {
558
+ return document.getElementById("alchemy_preview_window")
559
+ }
552
560
  }
553
561
 
554
562
  customElements.define("alchemy-element-editor", ElementEditor)
@@ -0,0 +1,87 @@
1
+ import SortableElements from "alchemy_admin/sortable_elements"
2
+
3
+ class ElementsWindow extends HTMLElement {
4
+ #visible = true
5
+
6
+ constructor() {
7
+ super()
8
+ this.#attachEvents()
9
+ }
10
+
11
+ connectedCallback() {
12
+ this.toggleButton?.addEventListener("click", (evt) => {
13
+ evt.preventDefault()
14
+ this.toggle()
15
+ })
16
+ if (window.location.hash) {
17
+ document
18
+ .querySelector(window.location.hash)
19
+ ?.trigger("FocusElementEditor.Alchemy")
20
+ }
21
+ SortableElements()
22
+ }
23
+
24
+ collapseAllElements() {
25
+ this.querySelectorAll(
26
+ "alchemy-element-editor:not([compact]):not([fixed])"
27
+ ).forEach((editor) => editor.collapse())
28
+ }
29
+
30
+ toggle() {
31
+ this.#visible ? this.hide() : this.show()
32
+ }
33
+
34
+ show() {
35
+ document.body.classList.add("elements-window-visible")
36
+ this.#visible = true
37
+ this.toggleButton.closest("sl-tooltip").content = Alchemy.t("Hide elements")
38
+ this.toggleButton
39
+ .querySelector("alchemy-icon")
40
+ .setAttribute("name", "menu-unfold")
41
+ }
42
+
43
+ hide() {
44
+ document.body.classList.remove("elements-window-visible")
45
+ this.#visible = false
46
+ this.toggleButton.closest("sl-tooltip").content = Alchemy.t("Show elements")
47
+ this.toggleButton
48
+ .querySelector("alchemy-icon")
49
+ .setAttribute("name", "menu-fold")
50
+ }
51
+
52
+ get collapseButton() {
53
+ return this.querySelector("#collapse-all-elements-button")
54
+ }
55
+
56
+ get toggleButton() {
57
+ return document.querySelector("#element_window_button")
58
+ }
59
+
60
+ get previewWindow() {
61
+ return document.getElementById("alchemy_preview_window")
62
+ }
63
+
64
+ #attachEvents() {
65
+ this.collapseButton?.addEventListener("click", () => {
66
+ this.collapseAllElements()
67
+ })
68
+ window.addEventListener("message", (event) => {
69
+ const data = event.data
70
+ if (data?.message == "Alchemy.focusElementEditor") {
71
+ const element = document.getElementById(`element_${data.element_id}`)
72
+ this.show()
73
+ element?.focusElement()
74
+ }
75
+ })
76
+ document.body.addEventListener("click", (evt) => {
77
+ if (!evt.target.closest("alchemy-element-editor")) {
78
+ this.querySelectorAll("alchemy-element-editor").forEach((editor) => {
79
+ editor.classList.remove("selected")
80
+ })
81
+ this.previewWindow?.postMessage({ message: "Alchemy.blurElements" })
82
+ }
83
+ })
84
+ }
85
+ }
86
+
87
+ customElements.define("alchemy-elements-window", ElementsWindow)
@@ -0,0 +1,13 @@
1
+ import { growl } from "alchemy_admin/growler"
2
+
3
+ class Growl extends HTMLElement {
4
+ connectedCallback() {
5
+ growl(this.message, this.getAttribute("type") || "notice")
6
+ }
7
+
8
+ get message() {
9
+ return this.getAttribute("message") || this.innerHTML
10
+ }
11
+ }
12
+
13
+ customElements.define("alchemy-growl", Growl)