alchemy_cms 7.0.12 → 7.1.0.pre.b1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/backport.yml +36 -0
- data/.github/workflows/test.yml +3 -2
- data/.gitignore +1 -0
- data/.standard.yml +1 -1
- data/CHANGELOG.md +144 -22
- data/Gemfile +8 -10
- data/README.md +10 -8
- data/alchemy_cms.gemspec +4 -3
- data/app/assets/config/alchemy_manifest.js +0 -1
- data/app/assets/javascripts/alchemy/admin.js +1 -19
- data/app/assets/javascripts/alchemy/alchemy.confirm_dialog.js.coffee +2 -3
- data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +19 -34
- data/app/assets/javascripts/alchemy/alchemy.elements_window.js.coffee +38 -13
- data/app/assets/javascripts/alchemy/alchemy.file_progress.js.coffee +1 -1
- data/app/assets/javascripts/alchemy/alchemy.fixed_elements.js +32 -25
- data/app/assets/javascripts/alchemy/alchemy.growler.js.coffee +1 -1
- data/app/assets/javascripts/alchemy/alchemy.image_overlay.coffee +3 -5
- data/app/assets/javascripts/alchemy/alchemy.initializer.js.coffee +0 -57
- data/app/assets/javascripts/alchemy/alchemy.link_dialog.js.coffee +22 -63
- data/app/assets/javascripts/alchemy/alchemy.list_filter.js.coffee +2 -2
- data/app/assets/javascripts/alchemy/alchemy.preview.js.coffee +5 -4
- data/app/assets/javascripts/alchemy/alchemy.preview_window.js.coffee +5 -5
- data/app/assets/javascripts/alchemy/templates/index.js +0 -2
- data/app/assets/javascripts/alchemy/templates/node_folder.hbs +1 -1
- data/app/assets/javascripts/alchemy/templates/page.hbs +1 -1
- data/app/assets/javascripts/alchemy/templates/page_folder.hbs +2 -2
- data/app/assets/stylesheets/alchemy/_custom-properties.scss +82 -0
- data/app/assets/stylesheets/alchemy/_mixins.scss +38 -30
- data/app/assets/stylesheets/alchemy/_variables.scss +12 -5
- data/app/assets/stylesheets/alchemy/admin.scss +3 -4
- data/app/assets/stylesheets/alchemy/archive.scss +107 -50
- data/app/assets/stylesheets/alchemy/attachments.scss +5 -4
- data/app/assets/stylesheets/alchemy/buttons.scss +38 -164
- data/app/assets/stylesheets/alchemy/dashboard.scss +31 -6
- data/app/assets/stylesheets/alchemy/dialogs.scss +12 -28
- data/app/assets/stylesheets/alchemy/elements.scss +273 -282
- data/app/assets/stylesheets/alchemy/flash.scss +20 -12
- data/app/assets/stylesheets/alchemy/forms.scss +21 -34
- data/app/assets/stylesheets/alchemy/frame.scss +11 -32
- data/app/assets/stylesheets/alchemy/hints.scss +4 -62
- data/app/assets/stylesheets/alchemy/image_library.scss +36 -33
- data/app/assets/stylesheets/alchemy/labels.scss +4 -1
- data/app/assets/stylesheets/alchemy/menubar.scss +7 -6
- data/app/assets/stylesheets/alchemy/navigation.scss +27 -15
- data/app/assets/stylesheets/alchemy/nodes.scss +11 -7
- data/app/assets/stylesheets/alchemy/notices.scss +16 -4
- data/app/assets/stylesheets/alchemy/page-select.scss +10 -2
- data/app/assets/stylesheets/alchemy/pagination.scss +22 -13
- data/app/assets/stylesheets/alchemy/resource_info.scss +7 -5
- data/app/assets/stylesheets/alchemy/selects.scss +49 -42
- data/app/assets/stylesheets/alchemy/shoelace.scss +345 -0
- data/app/assets/stylesheets/alchemy/sitemap.scss +24 -14
- data/app/assets/stylesheets/alchemy/spinner.scss +9 -19
- data/app/assets/stylesheets/alchemy/tables.scss +16 -24
- data/app/assets/stylesheets/alchemy/tags.scss +4 -0
- data/app/assets/stylesheets/alchemy/toolbar.scss +29 -25
- data/app/assets/stylesheets/alchemy/upload.scss +140 -89
- data/app/assets/stylesheets/tinymce/skins/alchemy/skin.min.css.scss +80 -108
- data/app/components/alchemy/admin/node_select.rb +39 -0
- data/app/components/alchemy/admin/page_select.rb +42 -0
- data/app/components/alchemy/ingredients/audio_view.rb +1 -1
- data/app/components/alchemy/ingredients/base_view.rb +1 -1
- data/app/components/alchemy/ingredients/boolean_view.rb +1 -1
- data/app/components/alchemy/ingredients/datetime_view.rb +1 -1
- data/app/components/alchemy/ingredients/file_view.rb +1 -1
- data/app/components/alchemy/ingredients/headline_view.rb +7 -16
- data/app/components/alchemy/ingredients/link_view.rb +1 -1
- data/app/components/alchemy/ingredients/page_view.rb +1 -1
- data/app/components/alchemy/ingredients/picture_view.rb +1 -1
- data/app/components/alchemy/ingredients/richtext_view.rb +1 -1
- data/app/components/alchemy/ingredients/text_view.rb +1 -1
- data/app/components/alchemy/ingredients/video_view.rb +1 -1
- data/app/controllers/alchemy/admin/base_controller.rb +5 -6
- data/app/controllers/alchemy/admin/elements_controller.rb +63 -35
- data/app/controllers/alchemy/admin/languages_controller.rb +1 -2
- data/app/controllers/alchemy/base_controller.rb +4 -2
- data/app/controllers/concerns/alchemy/admin/current_language.rb +1 -5
- data/app/controllers/concerns/alchemy/admin/uploader_responses.rb +1 -1
- data/app/controllers/concerns/alchemy/site_redirects.rb +1 -1
- data/app/decorators/alchemy/element_editor.rb +0 -2
- data/app/helpers/alchemy/admin/attachments_helper.rb +6 -5
- data/app/helpers/alchemy/admin/base_helper.rb +17 -12
- data/app/helpers/alchemy/admin/ingredients_helper.rb +4 -1
- data/app/helpers/alchemy/admin/pages_helper.rb +5 -11
- data/app/helpers/alchemy/base_helper.rb +47 -13
- data/app/javascript/alchemy_admin/components/alchemy_html_element.js +129 -0
- data/app/javascript/alchemy_admin/components/button.js +59 -0
- data/app/javascript/alchemy_admin/components/char_counter.js +40 -0
- data/app/javascript/alchemy_admin/components/datepicker.js +39 -0
- data/app/javascript/alchemy_admin/components/dialog_link.js +45 -0
- data/app/javascript/alchemy_admin/components/element_editor/publish_element_button.js +36 -0
- data/app/javascript/alchemy_admin/components/element_editor.js +553 -0
- data/app/javascript/alchemy_admin/components/ingredient_group.js +54 -0
- data/app/javascript/alchemy_admin/components/link_buttons/link_button.js +48 -0
- data/app/javascript/alchemy_admin/components/link_buttons/unlink_button.js +38 -0
- data/app/javascript/alchemy_admin/components/link_buttons.js +79 -0
- data/app/javascript/alchemy_admin/components/node_select.js +45 -0
- data/app/javascript/alchemy_admin/components/overlay.js +18 -0
- data/app/javascript/alchemy_admin/components/page_select.js +63 -0
- data/app/javascript/alchemy_admin/components/remote_select.js +134 -0
- data/app/javascript/alchemy_admin/components/select.js +12 -0
- data/app/javascript/alchemy_admin/components/spinner.js +31 -0
- data/app/javascript/alchemy_admin/components/tinymce.js +146 -0
- data/app/javascript/alchemy_admin/components/uploader/file_upload.js +266 -0
- data/app/javascript/alchemy_admin/components/uploader/progress.js +258 -0
- data/app/javascript/alchemy_admin/components/uploader.js +132 -0
- data/app/javascript/alchemy_admin/dirty.js +49 -0
- data/app/javascript/alchemy_admin/file_editors.js +1 -1
- data/app/javascript/alchemy_admin/gui.js +14 -0
- data/app/javascript/alchemy_admin/i18n.js +12 -8
- data/app/javascript/alchemy_admin/image_cropper.js +6 -3
- data/app/javascript/alchemy_admin/image_loader.js +7 -15
- data/app/javascript/alchemy_admin/ingredient_anchor_link.js +2 -5
- data/app/javascript/alchemy_admin/initializer.js +65 -0
- data/app/javascript/alchemy_admin/locales/en.js +31 -0
- data/app/javascript/alchemy_admin/picture_editors.js +2 -2
- data/app/javascript/alchemy_admin/picture_selector.js +38 -0
- data/app/javascript/alchemy_admin/please_wait_overlay.js +8 -0
- data/app/javascript/alchemy_admin/sortable_elements.js +78 -0
- data/app/javascript/alchemy_admin/spinner.js +36 -0
- data/app/javascript/alchemy_admin/tags_autocomplete.js +46 -0
- data/app/javascript/alchemy_admin/utils/ajax.js +6 -5
- data/app/javascript/alchemy_admin/utils/dom_helpers.js +20 -0
- data/app/javascript/alchemy_admin/utils/format.js +11 -0
- data/app/javascript/alchemy_admin/utils/string_conversions.js +10 -0
- data/app/javascript/alchemy_admin.js +70 -13
- data/app/javascript/menubar.js +10 -0
- data/app/models/alchemy/attachment.rb +9 -11
- data/app/models/alchemy/element.rb +11 -0
- data/app/models/alchemy/ingredients/richtext.rb +1 -10
- data/app/models/alchemy/node.rb +4 -0
- data/app/models/alchemy/page/page_elements.rb +2 -11
- data/app/models/alchemy/page/page_natures.rb +10 -2
- data/app/models/alchemy/page.rb +9 -51
- data/app/models/alchemy/picture/url.rb +1 -9
- data/app/serializers/alchemy/page_tree_serializer.rb +2 -1
- data/app/services/alchemy/copy_page.rb +98 -0
- data/app/views/alchemy/_menubar.html.erb +17 -13
- data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +14 -10
- data/app/views/alchemy/admin/attachments/_attachment.html.erb +44 -36
- data/app/views/alchemy/admin/attachments/_replace_button.html.erb +15 -21
- data/app/views/alchemy/admin/attachments/archive_overlay.js.erb +0 -1
- data/app/views/alchemy/admin/attachments/assign.js.erb +1 -1
- data/app/views/alchemy/admin/attachments/index.html.erb +6 -4
- data/app/views/alchemy/admin/attachments/show.html.erb +8 -8
- data/app/views/alchemy/admin/clipboard/clear.js.erb +1 -1
- data/app/views/alchemy/admin/clipboard/index.html.erb +3 -7
- data/app/views/alchemy/admin/clipboard/insert.js.erb +1 -1
- data/app/views/alchemy/admin/crop.html.erb +1 -1
- data/app/views/alchemy/admin/dashboard/_locked_pages.html.erb +1 -1
- data/app/views/alchemy/admin/dashboard/index.html.erb +13 -11
- data/app/views/alchemy/admin/dashboard/info.html.erb +7 -7
- data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +21 -23
- data/app/views/alchemy/admin/elements/_element.html.erb +52 -44
- data/app/views/alchemy/admin/elements/_footer.html.erb +1 -1
- data/app/views/alchemy/admin/elements/_form.html.erb +1 -1
- data/app/views/alchemy/admin/elements/_header.html.erb +11 -12
- data/app/views/alchemy/admin/elements/_toolbar.html.erb +33 -45
- data/app/views/alchemy/admin/elements/create.js.erb +7 -15
- data/app/views/alchemy/admin/elements/destroy.js.erb +0 -2
- data/app/views/alchemy/admin/elements/index.html.erb +27 -24
- data/app/views/alchemy/admin/elements/new.html.erb +9 -11
- data/app/views/alchemy/admin/ingredients/_file_fields.html.erb +2 -2
- data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +3 -3
- data/app/views/alchemy/admin/ingredients/_video_fields.html.erb +1 -2
- data/app/views/alchemy/admin/languages/_form.html.erb +2 -3
- data/app/views/alchemy/admin/languages/_language.html.erb +15 -8
- data/app/views/alchemy/admin/languages/_table.html.erb +1 -0
- data/app/views/alchemy/admin/layoutpages/_layoutpage.html.erb +28 -16
- data/app/views/alchemy/admin/layoutpages/index.html.erb +2 -2
- data/app/views/alchemy/admin/legacy_page_urls/_legacy_page_url.html.erb +12 -8
- data/app/views/alchemy/admin/legacy_page_urls/_new.html.erb +1 -1
- data/app/views/alchemy/admin/nodes/_form.html.erb +20 -21
- data/app/views/alchemy/admin/nodes/_node.html.erb +39 -34
- data/app/views/alchemy/admin/nodes/index.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_anchor_link.html.erb +4 -4
- data/app/views/alchemy/admin/pages/_create_language_form.html.erb +2 -2
- data/app/views/alchemy/admin/pages/_current_page.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_external_link.html.erb +4 -4
- data/app/views/alchemy/admin/pages/_file_link.html.erb +5 -5
- data/app/views/alchemy/admin/pages/_form.html.erb +10 -21
- data/app/views/alchemy/admin/pages/_internal_link.html.erb +4 -4
- data/app/views/alchemy/admin/pages/_locked_page.html.erb +2 -2
- data/app/views/alchemy/admin/pages/_new_page_form.html.erb +4 -17
- data/app/views/alchemy/admin/pages/_page.html.erb +76 -72
- data/app/views/alchemy/admin/pages/_page_infos.html.erb +23 -7
- data/app/views/alchemy/admin/pages/_page_layout_filter.html.erb +2 -1
- data/app/views/alchemy/admin/pages/_page_status.html.erb +11 -21
- data/app/views/alchemy/admin/pages/_publication_fields.html.erb +2 -5
- data/app/views/alchemy/admin/pages/_table.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_table_row.html.erb +43 -39
- data/app/views/alchemy/admin/pages/_toolbar.html.erb +43 -38
- data/app/views/alchemy/admin/pages/configure.html.erb +12 -14
- data/app/views/alchemy/admin/pages/edit.html.erb +80 -103
- data/app/views/alchemy/admin/pages/info.html.erb +20 -11
- data/app/views/alchemy/admin/pages/link.html.erb +22 -16
- data/app/views/alchemy/admin/pages/new.html.erb +9 -11
- data/app/views/alchemy/admin/pages/unlock.js.erb +10 -3
- data/app/views/alchemy/admin/partials/_language_tree_select.html.erb +15 -13
- data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +3 -5
- data/app/views/alchemy/admin/partials/_routes.html.erb +10 -2
- data/app/views/alchemy/admin/partials/_site_select.html.erb +6 -5
- data/app/views/alchemy/admin/partials/_toolbar_button.html.erb +28 -23
- data/app/views/alchemy/admin/pictures/_archive.html.erb +5 -5
- data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +21 -23
- data/app/views/alchemy/admin/pictures/_infos.html.erb +2 -6
- data/app/views/alchemy/admin/pictures/_picture.html.erb +15 -17
- data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +17 -16
- data/app/views/alchemy/admin/pictures/_tag_list.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/archive_overlay.js.erb +1 -1
- data/app/views/alchemy/admin/pictures/assign.js.erb +1 -1
- data/app/views/alchemy/admin/pictures/index.html.erb +34 -30
- data/app/views/alchemy/admin/pictures/show.html.erb +3 -3
- data/app/views/alchemy/admin/resources/_filter.html.erb +2 -2
- data/app/views/alchemy/admin/resources/_form.html.erb +2 -2
- data/app/views/alchemy/admin/resources/_per_page_select.html.erb +1 -1
- data/app/views/alchemy/admin/resources/_resource.html.erb +16 -9
- data/app/views/alchemy/admin/resources/_table.html.erb +4 -1
- data/app/views/alchemy/admin/resources/index.html.erb +22 -19
- data/app/views/alchemy/admin/sites/index.html.erb +2 -1
- data/app/views/alchemy/admin/styleguide/index.html.erb +54 -28
- data/app/views/alchemy/admin/tags/_tag.html.erb +16 -18
- data/app/views/alchemy/admin/tags/index.html.erb +15 -12
- data/app/views/alchemy/admin/tinymce/_setup.html.erb +29 -0
- data/app/views/alchemy/admin/uploader/_button.html.erb +23 -29
- data/app/views/alchemy/admin/uploader/_setup.html.erb +3 -8
- data/app/views/alchemy/base/500.html.erb +1 -1
- data/app/views/alchemy/base/error_notice.js.erb +0 -1
- data/app/views/alchemy/ingredients/_boolean_editor.html.erb +1 -1
- data/app/views/alchemy/ingredients/_datetime_editor.html.erb +1 -1
- data/app/views/alchemy/ingredients/_file_editor.html.erb +5 -5
- data/app/views/alchemy/ingredients/_link_editor.html.erb +1 -1
- data/app/views/alchemy/ingredients/_node_editor.html.erb +6 -19
- data/app/views/alchemy/ingredients/_page_editor.html.erb +7 -19
- data/app/views/alchemy/ingredients/_picture_editor.html.erb +2 -2
- data/app/views/alchemy/ingredients/_richtext_editor.html.erb +6 -15
- data/app/views/alchemy/ingredients/_select_editor.html.erb +2 -1
- data/app/views/alchemy/ingredients/_text_editor.html.erb +1 -1
- data/app/views/alchemy/ingredients/shared/_anchor.html.erb +1 -1
- data/app/views/alchemy/ingredients/shared/_link_tools.html.erb +10 -20
- data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +42 -49
- data/app/views/kaminari/alchemy/_first_page.html.erb +4 -2
- data/app/views/kaminari/alchemy/_gap.html.erb +1 -1
- data/app/views/kaminari/alchemy/_last_page.html.erb +4 -2
- data/app/views/kaminari/alchemy/_next_page.html.erb +4 -2
- data/app/views/kaminari/alchemy/_prev_page.html.erb +4 -2
- data/app/views/layouts/alchemy/admin.html.erb +10 -29
- data/config/alchemy/modules.yml +30 -30
- data/config/importmap.rb +10 -1
- data/config/initializers/rails_live_reload.rb +13 -0
- data/config/locales/alchemy.en.yml +23 -9
- data/config/routes.rb +2 -1
- data/lib/alchemy/auth_accessors.rb +6 -1
- data/lib/alchemy/controller_actions.rb +17 -4
- data/lib/alchemy/dev_support/live_reload_watcher.rb +5 -0
- data/lib/alchemy/engine.rb +8 -2
- data/lib/alchemy/forms/builder.rb +18 -12
- data/lib/alchemy/modules.rb +2 -2
- data/lib/alchemy/resources_helper.rb +3 -3
- data/lib/alchemy/test_support/capybara_helpers.rb +8 -5
- data/lib/alchemy/test_support/rspec_matchers.rb +14 -0
- data/lib/alchemy/test_support/shared_uploader_examples.rb +1 -1
- data/lib/alchemy/tinymce.rb +8 -3
- data/lib/alchemy/version.rb +1 -1
- data/package.json +14 -5
- data/vendor/assets/fonts/remixicon.eot +0 -0
- data/vendor/assets/fonts/remixicon.svg +7816 -0
- data/vendor/assets/fonts/remixicon.ttf +0 -0
- data/vendor/assets/fonts/remixicon.woff +0 -0
- data/vendor/assets/fonts/remixicon.woff2 +0 -0
- data/vendor/assets/stylesheets/remixicon.scss +10480 -0
- metadata +85 -97
- data/.gem_release.yml +0 -9
- data/app/assets/javascripts/alchemy/alchemy.autocomplete.js.coffee +0 -30
- data/app/assets/javascripts/alchemy/alchemy.base.js.coffee +0 -53
- data/app/assets/javascripts/alchemy/alchemy.buttons.js.coffee +0 -45
- data/app/assets/javascripts/alchemy/alchemy.char_counter.js.coffee +0 -19
- data/app/assets/javascripts/alchemy/alchemy.dirty.js.coffee +0 -59
- data/app/assets/javascripts/alchemy/alchemy.dragndrop.js.coffee +0 -79
- data/app/assets/javascripts/alchemy/alchemy.element_editors.js.coffee +0 -267
- data/app/assets/javascripts/alchemy/alchemy.gui.js.coffee +0 -27
- data/app/assets/javascripts/alchemy/alchemy.spinner.js +0 -32
- data/app/assets/javascripts/alchemy/alchemy.tooltips.coffee +0 -10
- data/app/assets/javascripts/alchemy/alchemy.uploader.js.coffee +0 -131
- data/app/assets/javascripts/alchemy/menubar.js.coffee +0 -8
- data/app/assets/javascripts/alchemy/node_select.js +0 -39
- data/app/assets/javascripts/alchemy/page_select.js +0 -46
- data/app/assets/javascripts/alchemy/templates/node.hbs +0 -16
- data/app/assets/javascripts/alchemy/templates/spinner.hbs +0 -7
- data/app/assets/stylesheets/alchemy/jquery-ui.scss +0 -435
- data/app/javascript/alchemy_admin/datepicker.js +0 -40
- data/app/javascript/alchemy_admin/tinymce.js +0 -146
- data/app/javascript/alchemy_admin/translations.js +0 -32
- data/app/views/alchemy/admin/elements/fold.js.erb +0 -33
- data/app/views/alchemy/admin/elements/order.js.erb +0 -11
- data/app/views/alchemy/admin/elements/publish.js.erb +0 -21
- data/app/views/alchemy/admin/elements/update.js.erb +0 -27
- data/vendor/assets/fonts/fa-regular-400.eot +0 -0
- data/vendor/assets/fonts/fa-regular-400.svg +0 -803
- data/vendor/assets/fonts/fa-regular-400.ttf +0 -0
- data/vendor/assets/fonts/fa-regular-400.woff +0 -0
- data/vendor/assets/fonts/fa-regular-400.woff2 +0 -0
- data/vendor/assets/fonts/fa-solid-900.eot +0 -0
- data/vendor/assets/fonts/fa-solid-900.svg +0 -4938
- data/vendor/assets/fonts/fa-solid-900.ttf +0 -0
- data/vendor/assets/fonts/fa-solid-900.woff +0 -0
- data/vendor/assets/fonts/fa-solid-900.woff2 +0 -0
- data/vendor/assets/javascripts/fileupload/jquery.fileupload-process.js +0 -178
- data/vendor/assets/javascripts/fileupload/jquery.fileupload-validate.js +0 -125
- data/vendor/assets/javascripts/fileupload/jquery.fileupload.js +0 -1502
- data/vendor/assets/javascripts/fileupload/jquery.iframe-transport.js +0 -224
- data/vendor/assets/javascripts/jquery-ui/data.js +0 -45
- data/vendor/assets/javascripts/jquery-ui/ie.js +0 -20
- data/vendor/assets/javascripts/jquery-ui/keycode.js +0 -51
- data/vendor/assets/javascripts/jquery-ui/plugin.js +0 -49
- data/vendor/assets/javascripts/jquery-ui/safe-active-element.js +0 -46
- data/vendor/assets/javascripts/jquery-ui/safe-blur.js +0 -27
- data/vendor/assets/javascripts/jquery-ui/scroll-parent.js +0 -50
- data/vendor/assets/javascripts/jquery-ui/unique-id.js +0 -54
- data/vendor/assets/javascripts/jquery-ui/version.js +0 -20
- data/vendor/assets/javascripts/jquery-ui/widget.js +0 -754
- data/vendor/assets/javascripts/jquery-ui/widgets/draggable.js +0 -1268
- data/vendor/assets/javascripts/jquery-ui/widgets/mouse.js +0 -241
- data/vendor/assets/javascripts/jquery-ui/widgets/sortable.js +0 -1623
- data/vendor/assets/javascripts/jquery-ui/widgets/tabs.js +0 -931
- data/vendor/assets/javascripts/jquery_plugins/jquery.scrollTo.min.js +0 -7
- data/vendor/assets/javascripts/jquery_plugins/jquery.ui.tabspaging.js +0 -296
- data/vendor/assets/stylesheets/fontawesome/_animated.scss +0 -20
- data/vendor/assets/stylesheets/fontawesome/_bordered-pulled.scss +0 -20
- data/vendor/assets/stylesheets/fontawesome/_core.scss +0 -21
- data/vendor/assets/stylesheets/fontawesome/_fixed-width.scss +0 -6
- data/vendor/assets/stylesheets/fontawesome/_icons.scss +0 -1441
- data/vendor/assets/stylesheets/fontawesome/_larger.scss +0 -23
- data/vendor/assets/stylesheets/fontawesome/_list.scss +0 -18
- data/vendor/assets/stylesheets/fontawesome/_mixins.scss +0 -56
- data/vendor/assets/stylesheets/fontawesome/_rotated-flipped.scss +0 -24
- data/vendor/assets/stylesheets/fontawesome/_screen-reader.scss +0 -5
- data/vendor/assets/stylesheets/fontawesome/_stacked.scss +0 -31
- data/vendor/assets/stylesheets/fontawesome/_variables.scss +0 -1458
- data/vendor/assets/stylesheets/fontawesome/fontawesome.scss +0 -16
- data/vendor/assets/stylesheets/fontawesome/regular.scss +0 -23
- 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
|
-
|
19
|
+
this.container.closest("alchemy-element-editor").setDirty()
|
20
20
|
return false
|
21
21
|
}
|
22
22
|
}
|