alchemy_cms 7.0.16 → 7.1.0.pre.b1

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 (359) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/backport.yml +36 -0
  3. data/.github/workflows/brakeman-analysis.yml +5 -13
  4. data/.github/workflows/lint.yml +2 -9
  5. data/.github/workflows/stale.yml +2 -5
  6. data/.github/workflows/test.yml +7 -15
  7. data/.gitignore +0 -1
  8. data/.standard.yml +1 -1
  9. data/CHANGELOG.md +144 -51
  10. data/Gemfile +7 -18
  11. data/README.md +10 -8
  12. data/alchemy_cms.gemspec +4 -3
  13. data/app/assets/config/alchemy_manifest.js +0 -1
  14. data/app/assets/javascripts/alchemy/admin.js +1 -19
  15. data/app/assets/javascripts/alchemy/alchemy.confirm_dialog.js.coffee +2 -3
  16. data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +19 -34
  17. data/app/assets/javascripts/alchemy/alchemy.elements_window.js.coffee +38 -13
  18. data/app/assets/javascripts/alchemy/alchemy.file_progress.js.coffee +1 -1
  19. data/app/assets/javascripts/alchemy/alchemy.fixed_elements.js +32 -25
  20. data/app/assets/javascripts/alchemy/alchemy.growler.js.coffee +1 -1
  21. data/app/assets/javascripts/alchemy/alchemy.image_overlay.coffee +3 -5
  22. data/app/assets/javascripts/alchemy/alchemy.initializer.js.coffee +0 -57
  23. data/app/assets/javascripts/alchemy/alchemy.link_dialog.js.coffee +22 -63
  24. data/app/assets/javascripts/alchemy/alchemy.list_filter.js.coffee +2 -2
  25. data/app/assets/javascripts/alchemy/alchemy.preview.js.coffee +5 -4
  26. data/app/assets/javascripts/alchemy/alchemy.preview_window.js.coffee +5 -5
  27. data/app/assets/javascripts/alchemy/templates/index.js +0 -2
  28. data/app/assets/javascripts/alchemy/templates/node_folder.hbs +1 -1
  29. data/app/assets/javascripts/alchemy/templates/page.hbs +1 -1
  30. data/app/assets/javascripts/alchemy/templates/page_folder.hbs +2 -2
  31. data/app/assets/stylesheets/alchemy/_custom-properties.scss +82 -0
  32. data/app/assets/stylesheets/alchemy/_mixins.scss +38 -30
  33. data/app/assets/stylesheets/alchemy/_variables.scss +12 -5
  34. data/app/assets/stylesheets/alchemy/admin.scss +3 -4
  35. data/app/assets/stylesheets/alchemy/archive.scss +107 -50
  36. data/app/assets/stylesheets/alchemy/attachments.scss +5 -4
  37. data/app/assets/stylesheets/alchemy/buttons.scss +38 -164
  38. data/app/assets/stylesheets/alchemy/dashboard.scss +31 -6
  39. data/app/assets/stylesheets/alchemy/dialogs.scss +12 -28
  40. data/app/assets/stylesheets/alchemy/elements.scss +273 -282
  41. data/app/assets/stylesheets/alchemy/flash.scss +20 -12
  42. data/app/assets/stylesheets/alchemy/forms.scss +21 -34
  43. data/app/assets/stylesheets/alchemy/frame.scss +11 -32
  44. data/app/assets/stylesheets/alchemy/hints.scss +4 -62
  45. data/app/assets/stylesheets/alchemy/image_library.scss +36 -33
  46. data/app/assets/stylesheets/alchemy/labels.scss +4 -1
  47. data/app/assets/stylesheets/alchemy/menubar.scss +7 -6
  48. data/app/assets/stylesheets/alchemy/navigation.scss +27 -15
  49. data/app/assets/stylesheets/alchemy/nodes.scss +11 -7
  50. data/app/assets/stylesheets/alchemy/notices.scss +16 -4
  51. data/app/assets/stylesheets/alchemy/page-select.scss +10 -2
  52. data/app/assets/stylesheets/alchemy/pagination.scss +22 -13
  53. data/app/assets/stylesheets/alchemy/preview_window.scss +4 -8
  54. data/app/assets/stylesheets/alchemy/resource_info.scss +7 -5
  55. data/app/assets/stylesheets/alchemy/selects.scss +49 -42
  56. data/app/assets/stylesheets/alchemy/shoelace.scss +345 -0
  57. data/app/assets/stylesheets/alchemy/sitemap.scss +24 -14
  58. data/app/assets/stylesheets/alchemy/spinner.scss +9 -19
  59. data/app/assets/stylesheets/alchemy/tables.scss +16 -24
  60. data/app/assets/stylesheets/alchemy/tags.scss +4 -0
  61. data/app/assets/stylesheets/alchemy/toolbar.scss +29 -25
  62. data/app/assets/stylesheets/alchemy/upload.scss +140 -89
  63. data/app/assets/stylesheets/tinymce/skins/alchemy/skin.min.css.scss +80 -108
  64. data/app/components/alchemy/admin/node_select.rb +39 -0
  65. data/app/components/alchemy/admin/page_select.rb +42 -0
  66. data/app/components/alchemy/ingredients/audio_view.rb +1 -1
  67. data/app/components/alchemy/ingredients/base_view.rb +1 -1
  68. data/app/components/alchemy/ingredients/boolean_view.rb +1 -1
  69. data/app/components/alchemy/ingredients/datetime_view.rb +3 -4
  70. data/app/components/alchemy/ingredients/file_view.rb +1 -1
  71. data/app/components/alchemy/ingredients/headline_view.rb +7 -16
  72. data/app/components/alchemy/ingredients/link_view.rb +1 -1
  73. data/app/components/alchemy/ingredients/page_view.rb +1 -1
  74. data/app/components/alchemy/ingredients/picture_view.rb +1 -1
  75. data/app/components/alchemy/ingredients/richtext_view.rb +1 -1
  76. data/app/components/alchemy/ingredients/text_view.rb +1 -1
  77. data/app/components/alchemy/ingredients/video_view.rb +1 -1
  78. data/app/controllers/alchemy/admin/base_controller.rb +7 -32
  79. data/app/controllers/alchemy/admin/elements_controller.rb +63 -35
  80. data/app/controllers/alchemy/admin/languages_controller.rb +2 -3
  81. data/app/controllers/alchemy/admin/layoutpages_controller.rb +0 -19
  82. data/app/controllers/alchemy/admin/pages_controller.rb +4 -5
  83. data/app/controllers/alchemy/admin/resources_controller.rb +1 -1
  84. data/app/controllers/alchemy/base_controller.rb +4 -2
  85. data/app/controllers/alchemy/messages_controller.rb +1 -1
  86. data/app/controllers/concerns/alchemy/admin/current_language.rb +1 -5
  87. data/app/controllers/concerns/alchemy/admin/uploader_responses.rb +1 -1
  88. data/app/decorators/alchemy/element_editor.rb +0 -2
  89. data/app/helpers/alchemy/admin/attachments_helper.rb +6 -5
  90. data/app/helpers/alchemy/admin/base_helper.rb +17 -12
  91. data/app/helpers/alchemy/admin/ingredients_helper.rb +4 -1
  92. data/app/helpers/alchemy/admin/pages_helper.rb +5 -11
  93. data/app/helpers/alchemy/base_helper.rb +47 -13
  94. data/app/javascript/alchemy_admin/components/alchemy_html_element.js +129 -0
  95. data/app/javascript/alchemy_admin/components/button.js +59 -0
  96. data/app/javascript/alchemy_admin/components/char_counter.js +40 -0
  97. data/app/javascript/alchemy_admin/components/datepicker.js +39 -0
  98. data/app/javascript/alchemy_admin/components/dialog_link.js +45 -0
  99. data/app/javascript/alchemy_admin/components/element_editor/publish_element_button.js +36 -0
  100. data/app/javascript/alchemy_admin/components/element_editor.js +553 -0
  101. data/app/javascript/alchemy_admin/components/ingredient_group.js +54 -0
  102. data/app/javascript/alchemy_admin/components/link_buttons/link_button.js +48 -0
  103. data/app/javascript/alchemy_admin/components/link_buttons/unlink_button.js +38 -0
  104. data/app/javascript/alchemy_admin/components/link_buttons.js +79 -0
  105. data/app/javascript/alchemy_admin/components/node_select.js +45 -0
  106. data/app/javascript/alchemy_admin/components/overlay.js +18 -0
  107. data/app/javascript/alchemy_admin/components/page_select.js +63 -0
  108. data/app/javascript/alchemy_admin/components/remote_select.js +134 -0
  109. data/app/javascript/alchemy_admin/components/select.js +12 -0
  110. data/app/javascript/alchemy_admin/components/spinner.js +31 -0
  111. data/app/javascript/alchemy_admin/components/tinymce.js +146 -0
  112. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +266 -0
  113. data/app/javascript/alchemy_admin/components/uploader/progress.js +258 -0
  114. data/app/javascript/alchemy_admin/components/uploader.js +132 -0
  115. data/app/javascript/alchemy_admin/dirty.js +49 -0
  116. data/app/javascript/alchemy_admin/file_editors.js +1 -1
  117. data/app/javascript/alchemy_admin/gui.js +14 -0
  118. data/app/javascript/alchemy_admin/i18n.js +12 -8
  119. data/app/javascript/alchemy_admin/image_cropper.js +6 -3
  120. data/app/javascript/alchemy_admin/image_loader.js +7 -15
  121. data/app/javascript/alchemy_admin/ingredient_anchor_link.js +2 -5
  122. data/app/javascript/alchemy_admin/initializer.js +65 -0
  123. data/app/javascript/alchemy_admin/locales/en.js +31 -0
  124. data/app/javascript/alchemy_admin/picture_editors.js +2 -2
  125. data/app/javascript/alchemy_admin/picture_selector.js +38 -0
  126. data/app/javascript/alchemy_admin/please_wait_overlay.js +8 -0
  127. data/app/javascript/alchemy_admin/sortable_elements.js +78 -0
  128. data/app/javascript/alchemy_admin/spinner.js +36 -0
  129. data/app/javascript/alchemy_admin/tags_autocomplete.js +46 -0
  130. data/app/javascript/alchemy_admin/utils/ajax.js +6 -5
  131. data/app/javascript/alchemy_admin/utils/dom_helpers.js +20 -0
  132. data/app/javascript/alchemy_admin/utils/format.js +11 -0
  133. data/app/javascript/alchemy_admin/utils/string_conversions.js +10 -0
  134. data/app/javascript/alchemy_admin.js +70 -13
  135. data/app/javascript/menubar.js +10 -0
  136. data/app/models/alchemy/attachment.rb +9 -11
  137. data/app/models/alchemy/element.rb +11 -0
  138. data/app/models/alchemy/ingredients/audio.rb +0 -11
  139. data/app/models/alchemy/ingredients/datetime.rb +1 -1
  140. data/app/models/alchemy/ingredients/richtext.rb +1 -10
  141. data/app/models/alchemy/ingredients/video.rb +0 -12
  142. data/app/models/alchemy/node.rb +4 -0
  143. data/app/models/alchemy/page/page_elements.rb +2 -11
  144. data/app/models/alchemy/page/page_natures.rb +10 -2
  145. data/app/models/alchemy/page.rb +12 -54
  146. data/app/models/alchemy/picture/url.rb +1 -9
  147. data/app/models/concerns/alchemy/picture_thumbnails.rb +5 -4
  148. data/app/serializers/alchemy/page_tree_serializer.rb +2 -1
  149. data/app/services/alchemy/copy_page.rb +98 -0
  150. data/app/views/alchemy/_menubar.html.erb +17 -13
  151. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +14 -10
  152. data/app/views/alchemy/admin/attachments/_attachment.html.erb +44 -36
  153. data/app/views/alchemy/admin/attachments/_replace_button.html.erb +15 -21
  154. data/app/views/alchemy/admin/attachments/archive_overlay.js.erb +0 -1
  155. data/app/views/alchemy/admin/attachments/assign.js.erb +1 -1
  156. data/app/views/alchemy/admin/attachments/index.html.erb +6 -4
  157. data/app/views/alchemy/admin/attachments/show.html.erb +8 -8
  158. data/app/views/alchemy/admin/clipboard/clear.js.erb +1 -1
  159. data/app/views/alchemy/admin/clipboard/index.html.erb +3 -7
  160. data/app/views/alchemy/admin/clipboard/insert.js.erb +1 -1
  161. data/app/views/alchemy/admin/crop.html.erb +1 -1
  162. data/app/views/alchemy/admin/dashboard/_locked_pages.html.erb +1 -1
  163. data/app/views/alchemy/admin/dashboard/index.html.erb +13 -11
  164. data/app/views/alchemy/admin/dashboard/info.html.erb +7 -7
  165. data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +21 -23
  166. data/app/views/alchemy/admin/elements/_element.html.erb +52 -44
  167. data/app/views/alchemy/admin/elements/_footer.html.erb +1 -1
  168. data/app/views/alchemy/admin/elements/_form.html.erb +1 -1
  169. data/app/views/alchemy/admin/elements/_header.html.erb +11 -12
  170. data/app/views/alchemy/admin/elements/_toolbar.html.erb +33 -45
  171. data/app/views/alchemy/admin/elements/create.js.erb +7 -15
  172. data/app/views/alchemy/admin/elements/destroy.js.erb +0 -2
  173. data/app/views/alchemy/admin/elements/index.html.erb +27 -24
  174. data/app/views/alchemy/admin/elements/new.html.erb +9 -11
  175. data/app/views/alchemy/admin/ingredients/_file_fields.html.erb +2 -2
  176. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +3 -3
  177. data/app/views/alchemy/admin/ingredients/_video_fields.html.erb +1 -2
  178. data/app/views/alchemy/admin/languages/_form.html.erb +2 -3
  179. data/app/views/alchemy/admin/languages/_language.html.erb +15 -8
  180. data/app/views/alchemy/admin/languages/_table.html.erb +1 -0
  181. data/app/views/alchemy/admin/layoutpages/_layoutpage.html.erb +28 -16
  182. data/app/views/alchemy/admin/layoutpages/edit.html.erb +1 -1
  183. data/app/views/alchemy/admin/layoutpages/index.html.erb +2 -2
  184. data/app/views/alchemy/admin/legacy_page_urls/_legacy_page_url.html.erb +12 -8
  185. data/app/views/alchemy/admin/legacy_page_urls/_new.html.erb +1 -1
  186. data/app/views/alchemy/admin/nodes/_form.html.erb +20 -21
  187. data/app/views/alchemy/admin/nodes/_node.html.erb +39 -34
  188. data/app/views/alchemy/admin/nodes/index.html.erb +1 -1
  189. data/app/views/alchemy/admin/pages/_anchor_link.html.erb +4 -4
  190. data/app/views/alchemy/admin/pages/_create_language_form.html.erb +2 -2
  191. data/app/views/alchemy/admin/pages/_current_page.html.erb +1 -1
  192. data/app/views/alchemy/admin/pages/_external_link.html.erb +4 -4
  193. data/app/views/alchemy/admin/pages/_file_link.html.erb +5 -5
  194. data/app/views/alchemy/admin/pages/_form.html.erb +10 -21
  195. data/app/views/alchemy/admin/pages/_internal_link.html.erb +4 -4
  196. data/app/views/alchemy/admin/pages/_locked_page.html.erb +2 -2
  197. data/app/views/alchemy/admin/pages/_new_page_form.html.erb +4 -17
  198. data/app/views/alchemy/admin/pages/_page.html.erb +76 -72
  199. data/app/views/alchemy/admin/pages/_page_infos.html.erb +23 -7
  200. data/app/views/alchemy/admin/pages/_page_layout_filter.html.erb +2 -1
  201. data/app/views/alchemy/admin/pages/_page_status.html.erb +11 -21
  202. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +2 -5
  203. data/app/views/alchemy/admin/pages/_table.html.erb +1 -1
  204. data/app/views/alchemy/admin/pages/_table_row.html.erb +43 -39
  205. data/app/views/alchemy/admin/pages/_toolbar.html.erb +43 -38
  206. data/app/views/alchemy/admin/pages/configure.html.erb +12 -14
  207. data/app/views/alchemy/admin/pages/edit.html.erb +80 -103
  208. data/app/views/alchemy/admin/pages/info.html.erb +20 -11
  209. data/app/views/alchemy/admin/pages/link.html.erb +22 -16
  210. data/app/views/alchemy/admin/pages/new.html.erb +9 -11
  211. data/app/views/alchemy/admin/pages/unlock.js.erb +10 -3
  212. data/app/views/alchemy/admin/partials/_language_tree_select.html.erb +15 -13
  213. data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +3 -5
  214. data/app/views/alchemy/admin/partials/_routes.html.erb +10 -2
  215. data/app/views/alchemy/admin/partials/_site_select.html.erb +6 -5
  216. data/app/views/alchemy/admin/partials/_toolbar_button.html.erb +28 -23
  217. data/app/views/alchemy/admin/pictures/_archive.html.erb +5 -5
  218. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -1
  219. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +21 -23
  220. data/app/views/alchemy/admin/pictures/_infos.html.erb +2 -6
  221. data/app/views/alchemy/admin/pictures/_picture.html.erb +15 -17
  222. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +17 -16
  223. data/app/views/alchemy/admin/pictures/_tag_list.html.erb +1 -1
  224. data/app/views/alchemy/admin/pictures/archive_overlay.js.erb +1 -1
  225. data/app/views/alchemy/admin/pictures/assign.js.erb +1 -1
  226. data/app/views/alchemy/admin/pictures/index.html.erb +34 -30
  227. data/app/views/alchemy/admin/pictures/show.html.erb +3 -3
  228. data/app/views/alchemy/admin/resources/_filter.html.erb +2 -2
  229. data/app/views/alchemy/admin/resources/_form.html.erb +2 -2
  230. data/app/views/alchemy/admin/resources/_per_page_select.html.erb +1 -1
  231. data/app/views/alchemy/admin/resources/_resource.html.erb +16 -9
  232. data/app/views/alchemy/admin/resources/_table.html.erb +4 -1
  233. data/app/views/alchemy/admin/resources/index.html.erb +22 -19
  234. data/app/views/alchemy/admin/sites/index.html.erb +2 -1
  235. data/app/views/alchemy/admin/styleguide/index.html.erb +54 -28
  236. data/app/views/alchemy/admin/tags/_tag.html.erb +16 -18
  237. data/app/views/alchemy/admin/tags/index.html.erb +15 -12
  238. data/app/views/alchemy/admin/tinymce/_setup.html.erb +29 -0
  239. data/app/views/alchemy/admin/uploader/_button.html.erb +23 -29
  240. data/app/views/alchemy/admin/uploader/_setup.html.erb +3 -8
  241. data/app/views/alchemy/base/500.html.erb +1 -1
  242. data/app/views/alchemy/base/error_notice.js.erb +0 -1
  243. data/app/views/alchemy/ingredients/_boolean_editor.html.erb +1 -1
  244. data/app/views/alchemy/ingredients/_datetime_editor.html.erb +2 -3
  245. data/app/views/alchemy/ingredients/_file_editor.html.erb +5 -5
  246. data/app/views/alchemy/ingredients/_link_editor.html.erb +1 -1
  247. data/app/views/alchemy/ingredients/_node_editor.html.erb +6 -19
  248. data/app/views/alchemy/ingredients/_page_editor.html.erb +7 -19
  249. data/app/views/alchemy/ingredients/_picture_editor.html.erb +2 -2
  250. data/app/views/alchemy/ingredients/_richtext_editor.html.erb +6 -15
  251. data/app/views/alchemy/ingredients/_select_editor.html.erb +2 -1
  252. data/app/views/alchemy/ingredients/_text_editor.html.erb +1 -1
  253. data/app/views/alchemy/ingredients/shared/_anchor.html.erb +1 -1
  254. data/app/views/alchemy/ingredients/shared/_link_tools.html.erb +10 -20
  255. data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +42 -49
  256. data/app/views/kaminari/alchemy/_first_page.html.erb +4 -2
  257. data/app/views/kaminari/alchemy/_gap.html.erb +1 -1
  258. data/app/views/kaminari/alchemy/_last_page.html.erb +4 -2
  259. data/app/views/kaminari/alchemy/_next_page.html.erb +4 -2
  260. data/app/views/kaminari/alchemy/_prev_page.html.erb +4 -2
  261. data/app/views/layouts/alchemy/admin.html.erb +10 -29
  262. data/config/alchemy/modules.yml +30 -30
  263. data/config/importmap.rb +10 -1
  264. data/config/initializers/rails_live_reload.rb +13 -0
  265. data/config/locales/alchemy.en.yml +23 -9
  266. data/config/routes.rb +3 -2
  267. data/lib/alchemy/auth_accessors.rb +6 -1
  268. data/lib/alchemy/controller_actions.rb +17 -4
  269. data/lib/alchemy/dev_support/live_reload_watcher.rb +5 -0
  270. data/lib/alchemy/engine.rb +8 -2
  271. data/lib/alchemy/forms/builder.rb +18 -12
  272. data/lib/alchemy/modules.rb +2 -2
  273. data/lib/alchemy/permissions.rb +1 -1
  274. data/lib/alchemy/resources_helper.rb +3 -3
  275. data/lib/alchemy/routing_constraints.rb +1 -1
  276. data/lib/alchemy/test_support/capybara_helpers.rb +8 -5
  277. data/lib/alchemy/test_support/rspec_matchers.rb +14 -0
  278. data/lib/alchemy/test_support/shared_uploader_examples.rb +1 -1
  279. data/lib/alchemy/tinymce.rb +8 -3
  280. data/lib/alchemy/version.rb +1 -1
  281. data/lib/tasks/alchemy/tidy.rake +1 -0
  282. data/package.json +14 -5
  283. data/vendor/assets/fonts/remixicon.eot +0 -0
  284. data/vendor/assets/fonts/remixicon.svg +7816 -0
  285. data/vendor/assets/fonts/remixicon.ttf +0 -0
  286. data/vendor/assets/fonts/remixicon.woff +0 -0
  287. data/vendor/assets/fonts/remixicon.woff2 +0 -0
  288. data/vendor/assets/stylesheets/remixicon.scss +10480 -0
  289. metadata +87 -97
  290. data/.gem_release.yml +0 -8
  291. data/app/assets/javascripts/alchemy/alchemy.autocomplete.js.coffee +0 -30
  292. data/app/assets/javascripts/alchemy/alchemy.base.js.coffee +0 -53
  293. data/app/assets/javascripts/alchemy/alchemy.buttons.js.coffee +0 -45
  294. data/app/assets/javascripts/alchemy/alchemy.char_counter.js.coffee +0 -19
  295. data/app/assets/javascripts/alchemy/alchemy.dirty.js.coffee +0 -59
  296. data/app/assets/javascripts/alchemy/alchemy.dragndrop.js.coffee +0 -79
  297. data/app/assets/javascripts/alchemy/alchemy.element_editors.js.coffee +0 -267
  298. data/app/assets/javascripts/alchemy/alchemy.gui.js.coffee +0 -27
  299. data/app/assets/javascripts/alchemy/alchemy.spinner.js +0 -32
  300. data/app/assets/javascripts/alchemy/alchemy.tooltips.coffee +0 -10
  301. data/app/assets/javascripts/alchemy/alchemy.uploader.js.coffee +0 -131
  302. data/app/assets/javascripts/alchemy/menubar.js.coffee +0 -8
  303. data/app/assets/javascripts/alchemy/node_select.js +0 -39
  304. data/app/assets/javascripts/alchemy/page_select.js +0 -46
  305. data/app/assets/javascripts/alchemy/templates/node.hbs +0 -16
  306. data/app/assets/javascripts/alchemy/templates/spinner.hbs +0 -7
  307. data/app/assets/stylesheets/alchemy/jquery-ui.scss +0 -435
  308. data/app/javascript/alchemy_admin/datepicker.js +0 -40
  309. data/app/javascript/alchemy_admin/tinymce.js +0 -146
  310. data/app/javascript/alchemy_admin/translations.js +0 -32
  311. data/app/views/alchemy/admin/elements/fold.js.erb +0 -33
  312. data/app/views/alchemy/admin/elements/order.js.erb +0 -11
  313. data/app/views/alchemy/admin/elements/publish.js.erb +0 -21
  314. data/app/views/alchemy/admin/elements/update.js.erb +0 -27
  315. data/vendor/assets/fonts/fa-regular-400.eot +0 -0
  316. data/vendor/assets/fonts/fa-regular-400.svg +0 -803
  317. data/vendor/assets/fonts/fa-regular-400.ttf +0 -0
  318. data/vendor/assets/fonts/fa-regular-400.woff +0 -0
  319. data/vendor/assets/fonts/fa-regular-400.woff2 +0 -0
  320. data/vendor/assets/fonts/fa-solid-900.eot +0 -0
  321. data/vendor/assets/fonts/fa-solid-900.svg +0 -4938
  322. data/vendor/assets/fonts/fa-solid-900.ttf +0 -0
  323. data/vendor/assets/fonts/fa-solid-900.woff +0 -0
  324. data/vendor/assets/fonts/fa-solid-900.woff2 +0 -0
  325. data/vendor/assets/javascripts/fileupload/jquery.fileupload-process.js +0 -178
  326. data/vendor/assets/javascripts/fileupload/jquery.fileupload-validate.js +0 -125
  327. data/vendor/assets/javascripts/fileupload/jquery.fileupload.js +0 -1502
  328. data/vendor/assets/javascripts/fileupload/jquery.iframe-transport.js +0 -224
  329. data/vendor/assets/javascripts/jquery-ui/data.js +0 -45
  330. data/vendor/assets/javascripts/jquery-ui/ie.js +0 -20
  331. data/vendor/assets/javascripts/jquery-ui/keycode.js +0 -51
  332. data/vendor/assets/javascripts/jquery-ui/plugin.js +0 -49
  333. data/vendor/assets/javascripts/jquery-ui/safe-active-element.js +0 -46
  334. data/vendor/assets/javascripts/jquery-ui/safe-blur.js +0 -27
  335. data/vendor/assets/javascripts/jquery-ui/scroll-parent.js +0 -50
  336. data/vendor/assets/javascripts/jquery-ui/unique-id.js +0 -54
  337. data/vendor/assets/javascripts/jquery-ui/version.js +0 -20
  338. data/vendor/assets/javascripts/jquery-ui/widget.js +0 -754
  339. data/vendor/assets/javascripts/jquery-ui/widgets/draggable.js +0 -1268
  340. data/vendor/assets/javascripts/jquery-ui/widgets/mouse.js +0 -241
  341. data/vendor/assets/javascripts/jquery-ui/widgets/sortable.js +0 -1623
  342. data/vendor/assets/javascripts/jquery-ui/widgets/tabs.js +0 -931
  343. data/vendor/assets/javascripts/jquery_plugins/jquery.scrollTo.min.js +0 -7
  344. data/vendor/assets/javascripts/jquery_plugins/jquery.ui.tabspaging.js +0 -296
  345. data/vendor/assets/stylesheets/fontawesome/_animated.scss +0 -20
  346. data/vendor/assets/stylesheets/fontawesome/_bordered-pulled.scss +0 -20
  347. data/vendor/assets/stylesheets/fontawesome/_core.scss +0 -21
  348. data/vendor/assets/stylesheets/fontawesome/_fixed-width.scss +0 -6
  349. data/vendor/assets/stylesheets/fontawesome/_icons.scss +0 -1441
  350. data/vendor/assets/stylesheets/fontawesome/_larger.scss +0 -23
  351. data/vendor/assets/stylesheets/fontawesome/_list.scss +0 -18
  352. data/vendor/assets/stylesheets/fontawesome/_mixins.scss +0 -56
  353. data/vendor/assets/stylesheets/fontawesome/_rotated-flipped.scss +0 -24
  354. data/vendor/assets/stylesheets/fontawesome/_screen-reader.scss +0 -5
  355. data/vendor/assets/stylesheets/fontawesome/_stacked.scss +0 -31
  356. data/vendor/assets/stylesheets/fontawesome/_variables.scss +0 -1458
  357. data/vendor/assets/stylesheets/fontawesome/fontawesome.scss +0 -16
  358. data/vendor/assets/stylesheets/fontawesome/regular.scss +0 -23
  359. data/vendor/assets/stylesheets/fontawesome/solid.scss +0 -24
@@ -0,0 +1,266 @@
1
+ import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
2
+ import { formatFileSize } from "alchemy_admin/utils/format"
3
+ import { translate } from "alchemy_admin/i18n"
4
+
5
+ export class FileUpload extends AlchemyHTMLElement {
6
+ /**
7
+ * @param {File} file
8
+ * @param {XMLHttpRequest} request
9
+ */
10
+ constructor(file, request) {
11
+ super({})
12
+
13
+ this.file = file
14
+ this.request = request
15
+
16
+ this.progressEventLoaded = 0
17
+ this.progressEventTotal = file ? file.size : 0
18
+ this.className = "in-progress"
19
+ this.valid = true
20
+ this.value = 0
21
+
22
+ this._validateFile()
23
+ this._addRequestEventListener()
24
+ }
25
+
26
+ render() {
27
+ return `
28
+ <sl-progress-bar value="${this.value}"></sl-progress-bar>
29
+ <div class="description">
30
+ <span class="file-name">${this.file?.name}</span>
31
+ <span class="loaded-size">${this.loadedSize}</span>
32
+ <span class="error-message">${this.errorMessage}</span>
33
+ </div>
34
+ <sl-tooltip content="${translate("Abort upload")}">
35
+ <button class="icon_button" aria-label="${translate("Abort upload")}">
36
+ <i class="icon ri-close-line ri-fw"></i>
37
+ </button>
38
+ </sl-tooltip>
39
+ `
40
+ }
41
+
42
+ afterRender() {
43
+ this.querySelector("button").addEventListener("click", () => this.cancel())
44
+
45
+ if (this.file?.type.includes("image")) {
46
+ const reader = new FileReader()
47
+ reader.readAsDataURL(this.file)
48
+ reader.addEventListener("load", () => {
49
+ const image = new Image()
50
+ image.src = reader.result
51
+ this.prepend(image)
52
+ })
53
+ }
54
+ }
55
+
56
+ /**
57
+ * cancel the upload
58
+ */
59
+ cancel() {
60
+ if (!this.finished) {
61
+ this.status = "canceled"
62
+ this.request?.abort()
63
+ this.dispatchCustomEvent("FileUpload.Change")
64
+ }
65
+ }
66
+
67
+ /**
68
+ * validate given file with the `Alchemy.uploader_defaults` - configuration
69
+ * @private
70
+ */
71
+ _validateFile() {
72
+ const config = Alchemy.uploader_defaults
73
+ const maxFileSize = config.file_size_limit * Math.pow(1024, 2) // in Byte
74
+ let errorMessage = undefined
75
+
76
+ if (this.file?.size > maxFileSize) {
77
+ errorMessage = translate("Uploaded bytes exceed file size")
78
+ }
79
+
80
+ const fileConfiguration = this.file?.type.includes("image")
81
+ ? "allowed_filetype_pictures"
82
+ : "allowed_filetype_attachments"
83
+
84
+ const isFileFormatSupported =
85
+ config[fileConfiguration] === "*" ||
86
+ config[fileConfiguration].includes(
87
+ this.file?.type.replace(/^\w+\/(\w+)(\+\w+)?/i, "$1")
88
+ )
89
+
90
+ if (!isFileFormatSupported) {
91
+ errorMessage = translate("File type not allowed")
92
+ }
93
+
94
+ if (errorMessage) {
95
+ this.valid = false
96
+ this.errorMessage = errorMessage
97
+ }
98
+ }
99
+
100
+ /**
101
+ * register event listeners to react on request changes
102
+ * @private
103
+ */
104
+ _addRequestEventListener() {
105
+ // prevent errors if the component will be called without a request - object
106
+ if (!this.request) {
107
+ return
108
+ }
109
+
110
+ // update the progress bar and currently loaded size information
111
+ this.request.upload.onprogress = (progressEvent) => {
112
+ this.progressEvent = progressEvent
113
+ }
114
+
115
+ // triggers, when the upload is done
116
+ this.request.onload = () => {
117
+ if (this.request.status < 400) {
118
+ this.status = "successful"
119
+ Alchemy.growl(this.responseMessage)
120
+ } else {
121
+ this.status = "failed"
122
+ this.errorMessage = this.responseMessage
123
+ }
124
+ this.dispatchCustomEvent("FileUpload.Change")
125
+ }
126
+
127
+ // catch request errors
128
+ this.request.onerror = () => {
129
+ this.errorMessage = translate("An error occurred during the transaction")
130
+ }
131
+ }
132
+
133
+ /**
134
+ * @returns {boolean}
135
+ */
136
+ get active() {
137
+ return this.valid && this.status !== "canceled"
138
+ }
139
+
140
+ /**
141
+ * @returns {string}
142
+ */
143
+ get errorMessage() {
144
+ return this._errorMessage || ""
145
+ }
146
+
147
+ /**
148
+ * @param {string} message
149
+ */
150
+ set errorMessage(message) {
151
+ this._errorMessage = message
152
+ const errorMessageContainer = this.querySelector(".error-message")
153
+ if (errorMessageContainer) {
154
+ errorMessageContainer.textContent = message
155
+ }
156
+ Alchemy.growl(message, "error")
157
+ }
158
+
159
+ /**
160
+ * @returns {boolean}
161
+ */
162
+ get finished() {
163
+ return ["canceled", "successful", "failed"].includes(this.status)
164
+ }
165
+
166
+ /**
167
+ * format the loaded and total size and present that as a string
168
+ * @returns {string}
169
+ */
170
+ get loadedSize() {
171
+ return `${formatFileSize(this.progressEventLoaded)} / ${formatFileSize(
172
+ this.progressEventTotal
173
+ )}`
174
+ }
175
+
176
+ /**
177
+ * @returns {HTMLProgressElement|undefined}
178
+ */
179
+ get progressElement() {
180
+ return this.querySelector("sl-progress-bar")
181
+ }
182
+
183
+ /**
184
+ * @param {ProgressEvent} progressEvent
185
+ */
186
+ set progressEvent(progressEvent) {
187
+ this.progressEventLoaded = progressEvent.loaded
188
+ this.progressEventTotal = progressEvent.total
189
+
190
+ this.value = Math.round((progressEvent.loaded / progressEvent.total) * 100)
191
+ this.querySelector(".loaded-size").textContent = this.loadedSize
192
+ }
193
+
194
+ /**
195
+ * @returns {string}
196
+ */
197
+ get responseMessage() {
198
+ try {
199
+ const response = JSON.parse(this.request.responseText)
200
+ return response["message"]
201
+ } catch (error) {
202
+ return translate("Could not parse JSON result")
203
+ }
204
+ }
205
+
206
+ /**
207
+ * @returns {string}
208
+ */
209
+ get status() {
210
+ return this._status
211
+ }
212
+
213
+ /**
214
+ * @param {string} status
215
+ */
216
+ set status(status) {
217
+ this._status = status
218
+ this.className = status
219
+
220
+ this.progressElement?.toggleAttribute(
221
+ "indeterminate",
222
+ status === "upload-finished"
223
+ )
224
+ }
225
+
226
+ /**
227
+ * @returns {boolean}
228
+ */
229
+ get valid() {
230
+ return this._valid
231
+ }
232
+
233
+ /**
234
+ * @param {boolean} isValid
235
+ */
236
+ set valid(isValid) {
237
+ this._valid = isValid
238
+ this.classList.toggle("invalid", !isValid)
239
+ }
240
+
241
+ /**
242
+ * get the progress value of the current file
243
+ * @returns {number}
244
+ */
245
+ get value() {
246
+ return this._value
247
+ }
248
+
249
+ /**
250
+ * @param {number} value
251
+ */
252
+ set value(value) {
253
+ this._value = value
254
+ if (this.progressElement) {
255
+ this.progressElement.value = value
256
+ }
257
+
258
+ if (value === 100) {
259
+ this.status = "upload-finished"
260
+ }
261
+
262
+ this.dispatchCustomEvent("FileUpload.Change")
263
+ }
264
+ }
265
+
266
+ customElements.define("alchemy-file-upload", FileUpload)
@@ -0,0 +1,258 @@
1
+ import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
2
+ import { FileUpload } from "alchemy_admin/components/uploader/file_upload"
3
+ import { formatFileSize } from "alchemy_admin/utils/format"
4
+ import { translate } from "alchemy_admin/i18n"
5
+
6
+ export class Progress extends AlchemyHTMLElement {
7
+ #visible = false
8
+
9
+ /**
10
+ * @param {FileUpload[]} fileUploads
11
+ */
12
+ constructor(fileUploads = []) {
13
+ super()
14
+ this.buttonLabel = translate("Cancel all uploads")
15
+ this.fileUploads = fileUploads
16
+ this.fileCount = fileUploads.length
17
+ this.className = "in-progress"
18
+ this.visible = true
19
+ this.handleFileChange = () => this._updateView()
20
+ }
21
+
22
+ /**
23
+ * append file progress - components for each file
24
+ */
25
+ afterRender() {
26
+ this.actionButton = this.querySelector("button")
27
+ this.actionButton.addEventListener("click", () => {
28
+ if (this.finished) {
29
+ this.onComplete(this.status)
30
+ this.visible = false
31
+ } else {
32
+ this.cancel()
33
+ }
34
+ })
35
+
36
+ this.fileUploads.forEach((fileUpload) => {
37
+ this.querySelector(".single-uploads").append(fileUpload)
38
+ })
39
+ }
40
+
41
+ /**
42
+ * cancel requests in all remaining uploads
43
+ */
44
+ cancel() {
45
+ this._activeUploads().forEach((upload) => {
46
+ upload.cancel()
47
+ })
48
+ this._setupCloseButton()
49
+ }
50
+
51
+ /**
52
+ * update view and register change event
53
+ */
54
+ connected() {
55
+ this._updateView()
56
+ this.addEventListener("Alchemy.FileUpload.Change", this.handleFileChange)
57
+ }
58
+
59
+ /**
60
+ * deregister file upload change - event
61
+ */
62
+ disconnected() {
63
+ this.removeEventListener("Alchemy.FileUpload.Change", this.handleFileChange)
64
+ }
65
+
66
+ /**
67
+ * a complete hook to allow the uploader to react and trigger an event
68
+ * it would be possible to trigger the event here, but the dispatching would happen
69
+ * in the scope of that component and can't be cached o uploader - component level
70
+ */
71
+ onComplete(_status) {}
72
+
73
+ render() {
74
+ return `
75
+ <sl-progress-bar value="0"></sl-progress-bar>
76
+ <div class="overall-progress-value">
77
+ <span class="value-text"></span>
78
+
79
+ <sl-tooltip content="${this.buttonLabel}">
80
+ <button class="icon_button" aria-label="${this.buttonLabel}">
81
+ <i class="icon ri-close-line ri-fw"></i>
82
+ </button>
83
+ </sl-tooltip>
84
+ </div>
85
+ <div class="single-uploads" style="--progress-columns: ${
86
+ this.fileCount > 3 ? 3 : this.fileCount
87
+ }"></div>
88
+ <div class="overall-upload-value value-text"></div>
89
+ `
90
+ }
91
+
92
+ /**
93
+ * get all active upload components
94
+ * @returns {FileUpload[]}
95
+ * @private
96
+ */
97
+ _activeUploads() {
98
+ return this.fileUploads.filter((upload) => upload.active)
99
+ }
100
+
101
+ /**
102
+ * replace cancel button to be the close button
103
+ * @private
104
+ */
105
+ _setupCloseButton() {
106
+ this.buttonLabel = translate("Close")
107
+ this.actionButton.ariaLabel = this.buttonLabel
108
+ this.actionButton.parentElement.content = this.buttonLabel // update tooltip content
109
+ }
110
+
111
+ /**
112
+ * @param {string} field
113
+ * @returns {number}
114
+ * @private
115
+ */
116
+ _sumFileProgresses(field) {
117
+ return this._activeUploads().reduce(
118
+ (accumulator, upload) => upload[field] + accumulator,
119
+ 0
120
+ )
121
+ }
122
+
123
+ /**
124
+ * don't render the whole element new, because it would prevent selecting buttons
125
+ * @private
126
+ */
127
+ _updateView() {
128
+ const status = this.status
129
+
130
+ // update progress bar
131
+ this.progressElement.value = this.totalProgress
132
+ this.progressElement.toggleAttribute(
133
+ "indeterminate",
134
+ status === "upload-finished"
135
+ )
136
+
137
+ // show progress in file size and percentage
138
+ this.querySelector(`.overall-progress-value > span`).textContent =
139
+ this.overallProgressValue
140
+ this.querySelector(`.overall-upload-value`).textContent =
141
+ this.overallUploadSize
142
+
143
+ if (this.finished) {
144
+ this._setupCloseButton()
145
+ this.onComplete(status)
146
+ }
147
+
148
+ this.className = status
149
+ this.visible = true
150
+ }
151
+
152
+ /**
153
+ * @returns {boolean}
154
+ */
155
+ get finished() {
156
+ return this._activeUploads().every((entry) => entry.finished)
157
+ }
158
+
159
+ /**
160
+ * @returns {string}
161
+ */
162
+ get overallUploadSize() {
163
+ const uploadedFileCount = this._activeUploads().filter(
164
+ (fileProgress) => fileProgress.value >= 100
165
+ ).length
166
+ const overallProgressValue = `${
167
+ this.totalProgress
168
+ }% (${uploadedFileCount} / ${this._activeUploads().length})`
169
+
170
+ return `${formatFileSize(
171
+ this._sumFileProgresses("progressEventLoaded")
172
+ )} / ${formatFileSize(this._sumFileProgresses("progressEventTotal"))}`
173
+ }
174
+
175
+ /**
176
+ * @returns {string}
177
+ */
178
+ get overallProgressValue() {
179
+ const uploadedFileCount = this._activeUploads().filter(
180
+ (fileProgress) => fileProgress.value >= 100
181
+ ).length
182
+ return `${this.totalProgress}% (${uploadedFileCount} / ${
183
+ this._activeUploads().length
184
+ })`
185
+ }
186
+
187
+ /**
188
+ * @returns {HTMLProgressElement|undefined}
189
+ */
190
+ get progressElement() {
191
+ return this.querySelector("sl-progress-bar")
192
+ }
193
+
194
+ /**
195
+ * get status of file progresses and accumulate the overall status
196
+ * @returns {string}
197
+ */
198
+ get status() {
199
+ const uploadsStatuses = this._activeUploads().map(
200
+ (upload) => upload.className
201
+ )
202
+
203
+ // mark as failed, if any upload failed
204
+ if (uploadsStatuses.includes("failed")) {
205
+ return "failed"
206
+ }
207
+
208
+ // no active upload means that every upload was canceled
209
+ if (uploadsStatuses.length === 0) {
210
+ return "canceled"
211
+ }
212
+
213
+ // all uploads are successful or upload-finished or in-progress
214
+ if (uploadsStatuses.every((entry) => entry === uploadsStatuses[0])) {
215
+ return uploadsStatuses[0]
216
+ }
217
+
218
+ return "in-progress"
219
+ }
220
+
221
+ /**
222
+ * @returns {number}
223
+ */
224
+ get totalProgress() {
225
+ const totalSize = this._activeUploads().reduce(
226
+ (accumulator, upload) => accumulator + upload.file.size,
227
+ 0
228
+ )
229
+ let totalProgress = Math.ceil(
230
+ this._activeUploads().reduce((accumulator, upload) => {
231
+ const weight = upload.file.size / totalSize
232
+ return upload.value * weight + accumulator
233
+ }, 0)
234
+ )
235
+ // prevent rounding errors
236
+ if (totalProgress > 100) {
237
+ totalProgress = 100
238
+ }
239
+ return totalProgress
240
+ }
241
+
242
+ /**
243
+ * @returns {boolean}
244
+ */
245
+ get visible() {
246
+ return this.#visible
247
+ }
248
+
249
+ /**
250
+ * @param {boolean} visible
251
+ */
252
+ set visible(visible) {
253
+ this.classList.toggle("visible", visible)
254
+ this.#visible = visible
255
+ }
256
+ }
257
+
258
+ customElements.define("alchemy-upload-progress", Progress)
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @typedef {object} PersistedFile
3
+ * @property {string} name
4
+ * @property {number} size
5
+ */
6
+ import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
7
+ import { Progress } from "alchemy_admin/components/uploader/progress"
8
+ import { FileUpload } from "alchemy_admin/components/uploader/file_upload"
9
+ import { translate } from "alchemy_admin/i18n"
10
+ import { getToken } from "alchemy_admin/utils/ajax"
11
+
12
+ export class Uploader extends AlchemyHTMLElement {
13
+ static properties = {
14
+ dropzone: { default: false }
15
+ }
16
+
17
+ connected() {
18
+ this.fileInput.addEventListener("change", (event) => {
19
+ this._uploadFiles(Array.from(event.target.files))
20
+ })
21
+ if (this.dropzone) {
22
+ this._dragAndDropBehavior()
23
+ }
24
+ }
25
+
26
+ /**
27
+ * add dragover class to indicate, if the file is draggable
28
+ * @private
29
+ */
30
+ _dragAndDropBehavior() {
31
+ const dropzoneElement = document.querySelector(this.dropzone)
32
+ let isDraggedOver = false
33
+
34
+ const toggleDropzoneClass = (enabled) => {
35
+ if (isDraggedOver !== enabled) {
36
+ isDraggedOver = enabled
37
+ dropzoneElement.classList.toggle("dragover")
38
+ }
39
+ }
40
+
41
+ dropzoneElement.addEventListener("dragleave", () =>
42
+ toggleDropzoneClass(false)
43
+ )
44
+ dropzoneElement.addEventListener("drop", async (event) => {
45
+ event.preventDefault()
46
+ toggleDropzoneClass(false)
47
+
48
+ const files = [...event.dataTransfer.items].map((item) =>
49
+ item.getAsFile()
50
+ )
51
+
52
+ this._uploadFiles(files)
53
+ })
54
+
55
+ dropzoneElement.addEventListener("dragover", (event) => {
56
+ event.preventDefault() // dragover has to be disabled to use the custom drop event
57
+ toggleDropzoneClass(true)
58
+ })
59
+ }
60
+
61
+ /**
62
+ * @param {File[]} files
63
+ * @private
64
+ */
65
+ _uploadFiles(files) {
66
+ // prepare file progress bars and server request
67
+ let fileUploadCount = 0
68
+
69
+ const fileUploads = files.map((file) => {
70
+ const request = new XMLHttpRequest()
71
+ const fileUpload = new FileUpload(file, request)
72
+
73
+ if (Alchemy.uploader_defaults.upload_limit - 1 < fileUploadCount) {
74
+ fileUpload.valid = false
75
+ fileUpload.errorMessage = translate("Maximum number of files exceeded")
76
+ } else if (fileUpload.valid) {
77
+ fileUploadCount++
78
+ this._submitFile(request, file)
79
+ }
80
+
81
+ return fileUpload
82
+ })
83
+
84
+ this._createProgress(fileUploads)
85
+ }
86
+
87
+ /**
88
+ * @param {XMLHttpRequest} request
89
+ * @param {File} file
90
+ * @private
91
+ */
92
+ _submitFile(request, file) {
93
+ const form = this.querySelector("form")
94
+ const formData = new FormData(form)
95
+ formData.set(this.fileInput.name, file)
96
+ request.open("POST", form.action)
97
+ request.setRequestHeader("X-CSRF-Token", getToken())
98
+ request.setRequestHeader("X-Requested-With", "XMLHttpRequest")
99
+ request.setRequestHeader("Accept", "application/json")
100
+ request.send(formData)
101
+ }
102
+
103
+ /**
104
+ * create (and maybe remove the old) progress bar - component
105
+ * @param {FileUpload[]} fileUploads
106
+ * @private
107
+ */
108
+ _createProgress(fileUploads) {
109
+ if (this.uploadProgress) {
110
+ this.uploadProgress.cancel()
111
+ document.body.removeChild(this.uploadProgress)
112
+ }
113
+ this.uploadProgress = new Progress(fileUploads)
114
+ this.uploadProgress.onComplete = (status) => {
115
+ if (status === "successful" || status === "canceled") {
116
+ this.uploadProgress.visible = false
117
+ }
118
+ this.dispatchCustomEvent(`upload.${status}`)
119
+ }
120
+
121
+ document.body.append(this.uploadProgress)
122
+ }
123
+
124
+ /**
125
+ * @returns {HTMLInputElement}
126
+ */
127
+ get fileInput() {
128
+ return this.querySelector("input[type='file']")
129
+ }
130
+ }
131
+
132
+ customElements.define("alchemy-uploader", Uploader)
@@ -0,0 +1,49 @@
1
+ function isPageDirty() {
2
+ return $("#element_area").find("alchemy-element-editor.dirty").length > 0
3
+ }
4
+
5
+ function checkPageDirtyness(element) {
6
+ let callback = () => {}
7
+
8
+ if ($(element).is("form")) {
9
+ callback = function () {
10
+ const $form = $(
11
+ `<form action="${element.action}" method="POST" style="display: none" />`
12
+ )
13
+ $form.append($(element).find("input"))
14
+ $form.appendTo("body")
15
+
16
+ Alchemy.pleaseWaitOverlay()
17
+ $form.submit()
18
+ }
19
+ } else if ($(element).is("a")) {
20
+ callback = () => Turbo.visit(element.pathname)
21
+ }
22
+
23
+ if (isPageDirty()) {
24
+ Alchemy.openConfirmDialog(Alchemy.t("page_dirty_notice"), {
25
+ title: Alchemy.t("warning"),
26
+ ok_label: Alchemy.t("ok"),
27
+ cancel_label: Alchemy.t("cancel"),
28
+ on_ok: function () {
29
+ window.onbeforeunload = void 0
30
+ callback()
31
+ }
32
+ })
33
+ return false
34
+ }
35
+ return true
36
+ }
37
+
38
+ function PageLeaveObserver() {
39
+ $("#main_navi a").on("click", function (event) {
40
+ if (!checkPageDirtyness(event.currentTarget)) {
41
+ event.preventDefault()
42
+ }
43
+ })
44
+ }
45
+
46
+ export default {
47
+ checkPageDirtyness,
48
+ PageLeaveObserver
49
+ }
@@ -16,7 +16,7 @@ class FileEditor {
16
16
  this.fileIcon.innerHTML = ""
17
17
  this.fileName.innerHTML = ""
18
18
  this.deleteLink.classList.add("hidden")
19
- Alchemy.setElementDirty(this.container.closest(".element-editor"))
19
+ this.container.closest("alchemy-element-editor").setDirty()
20
20
  return false
21
21
  }
22
22
  }