alchemy_cms 8.3.10 → 8.4.0
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.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +58 -10
- data/app/assets/builds/alchemy/admin/page-select.css +1 -1
- data/app/assets/builds/alchemy/admin/print.css +1 -1
- data/app/assets/builds/alchemy/admin/remote-select.css +1 -0
- data/app/assets/builds/alchemy/admin/tags-autocomplete.css +1 -0
- data/app/assets/builds/alchemy/admin.css +4 -4
- data/app/assets/builds/alchemy/alchemy_admin.min.js +1 -1
- data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -1
- data/app/assets/builds/alchemy/dark-theme.css +1 -1
- data/app/assets/builds/alchemy/light-theme.css +1 -1
- data/app/assets/builds/alchemy/preview.min.js +1 -1
- data/app/assets/builds/alchemy/theme.css +1 -1
- data/app/assets/builds/alchemy/welcome.css +1 -1
- data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
- data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -1
- data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
- data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -1
- data/app/assets/images/alchemy/icons-sprite.svg +1 -1
- data/app/components/alchemy/admin/dashboard/widgets/greeting.html.erb +1 -1
- data/app/components/alchemy/admin/dashboard/widgets/news_reader.html.erb +38 -0
- data/app/components/alchemy/admin/dashboard/widgets/news_reader.rb +54 -0
- data/app/components/alchemy/admin/dashboard/widgets/online_users.html.erb +1 -1
- data/app/components/alchemy/admin/dashboard/widgets/recent_pages.html.erb +1 -1
- data/app/components/alchemy/admin/dashboard/widgets/system_info.html.erb +11 -3
- data/app/components/alchemy/admin/dashboard/widgets/system_info.rb +6 -1
- data/app/components/alchemy/admin/element_editor.html.erb +1 -1
- data/app/components/alchemy/admin/element_select.rb +30 -20
- data/app/components/alchemy/admin/element_status_indicators.html.erb +16 -0
- data/app/components/alchemy/admin/element_status_indicators.rb +31 -0
- data/app/components/alchemy/admin/link_dialog/external_tab.rb +2 -1
- data/app/components/alchemy/admin/main_navigation/entry.rb +61 -0
- data/app/components/alchemy/admin/main_navigation/menu.rb +21 -0
- data/app/components/alchemy/admin/main_navigation/sub_navigation.rb +31 -0
- data/app/components/alchemy/admin/page_node.html.erb +1 -1
- data/app/components/alchemy/admin/page_permitted_roles_select.html.erb +11 -0
- data/app/components/alchemy/admin/page_permitted_roles_select.rb +35 -0
- data/app/components/alchemy/admin/page_select.rb +18 -8
- data/app/components/alchemy/admin/preview_time_select.rb +1 -1
- data/app/components/alchemy/admin/toolbar_button.rb +43 -20
- data/app/components/alchemy/admin/uploader_button.rb +66 -0
- data/app/components/alchemy/ingredients/base_editor.rb +10 -1
- data/app/components/alchemy/ingredients/boolean_editor.rb +1 -0
- data/app/components/alchemy/ingredients/color_editor.rb +6 -3
- data/app/components/alchemy/ingredients/datetime_view.rb +1 -1
- data/app/components/alchemy/ingredients/picture_view.rb +2 -4
- data/app/components/alchemy/menubar.html.erb +157 -0
- data/app/components/alchemy/menubar.rb +15 -0
- data/app/controllers/alchemy/admin/attachments_controller.rb +12 -9
- data/app/controllers/alchemy/admin/base_controller.rb +34 -21
- data/app/controllers/alchemy/admin/dashboard_controller.rb +4 -0
- data/app/controllers/alchemy/admin/elements_controller.rb +4 -23
- data/app/controllers/alchemy/admin/languages_controller.rb +1 -1
- data/app/controllers/alchemy/admin/nodes_controller.rb +1 -1
- data/app/controllers/alchemy/admin/pages_controller.rb +1 -1
- data/app/controllers/alchemy/admin/pictures_controller.rb +6 -3
- data/app/controllers/alchemy/admin/resources_controller.rb +5 -5
- data/app/controllers/alchemy/admin/sites_controller.rb +1 -1
- data/app/controllers/alchemy/base_controller.rb +24 -16
- data/app/controllers/alchemy/pages_controller.rb +29 -8
- data/app/controllers/alchemy/sitemap_controller.rb +58 -0
- data/app/controllers/concerns/alchemy/admin/csp_protection.rb +43 -0
- data/app/controllers/concerns/alchemy/admin/current_language.rb +1 -1
- data/app/controllers/concerns/alchemy/admin/resource_filter.rb +3 -1
- data/{lib → app/controllers/concerns}/alchemy/controller_actions.rb +13 -0
- data/app/controllers/concerns/alchemy/legacy_page_redirects.rb +4 -4
- data/app/helpers/alchemy/admin/base_helper.rb +4 -1
- data/app/helpers/alchemy/admin/form_helper.rb +0 -2
- data/app/helpers/alchemy/admin/navigation_helper.rb +10 -181
- data/app/helpers/alchemy/elements_block_helper.rb +14 -0
- data/app/helpers/alchemy/resources_helper.rb +15 -1
- data/app/javascript/alchemy_admin/components/attachment_select.js +11 -20
- data/app/javascript/alchemy_admin/components/auto_submit.js +2 -4
- data/app/javascript/alchemy_admin/components/button.js +0 -6
- data/app/javascript/alchemy_admin/components/clear_schedule_button.js +21 -0
- data/app/javascript/alchemy_admin/components/color_select.js +50 -45
- data/app/javascript/alchemy_admin/components/conditional_field.js +50 -0
- data/app/javascript/alchemy_admin/components/dirty_guard.js +77 -0
- data/app/javascript/alchemy_admin/components/dom_id_select.js +17 -10
- data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +7 -2
- data/app/javascript/alchemy_admin/components/element_editor.js +29 -56
- data/app/javascript/alchemy_admin/components/element_select.js +26 -66
- data/app/javascript/alchemy_admin/components/icon.js +9 -4
- data/app/javascript/alchemy_admin/{image_cropper.js → components/image_cropper.js} +39 -53
- data/app/javascript/alchemy_admin/components/image_overlay_link.js +14 -0
- data/app/javascript/alchemy_admin/components/index.js +12 -1
- data/app/javascript/alchemy_admin/components/main_navi_entry.js +75 -0
- data/app/javascript/alchemy_admin/components/menu_node.js +73 -0
- data/app/javascript/alchemy_admin/components/news_reader.js +109 -0
- data/app/javascript/alchemy_admin/components/node_folder.js +36 -0
- data/app/javascript/alchemy_admin/components/node_form.js +34 -0
- data/app/javascript/alchemy_admin/components/node_select.js +25 -30
- data/app/javascript/alchemy_admin/components/node_tree.js +56 -0
- data/app/javascript/alchemy_admin/components/page_node.js +2 -1
- data/app/javascript/alchemy_admin/components/page_select.js +16 -34
- data/app/javascript/alchemy_admin/components/picture_archive.js +92 -0
- data/app/javascript/alchemy_admin/components/picture_editor.js +2 -1
- data/app/javascript/alchemy_admin/components/preview_window.js +11 -3
- data/app/javascript/alchemy_admin/components/remote_select.js +461 -81
- data/app/javascript/alchemy_admin/components/select.js +26 -88
- data/app/javascript/alchemy_admin/components/sitemap.js +2 -1
- data/app/javascript/alchemy_admin/components/sortable_elements.js +2 -1
- data/app/javascript/alchemy_admin/components/tags_autocomplete.js +63 -48
- data/app/javascript/alchemy_admin/components/tinymce.js +2 -1
- data/app/javascript/alchemy_admin/components/uploader/file_upload.js +3 -2
- data/app/javascript/alchemy_admin/components/uploader.js +19 -3
- data/app/javascript/alchemy_admin/confirm_dialog.js +7 -1
- data/app/javascript/alchemy_admin/dialog.js +182 -110
- data/app/javascript/alchemy_admin/hotkeys.js +0 -5
- data/app/javascript/alchemy_admin/i18n.js +3 -9
- data/app/javascript/alchemy_admin/image_overlay.js +58 -42
- data/app/javascript/alchemy_admin/initializer.js +2 -2
- data/app/javascript/alchemy_admin/link_dialog.js +15 -2
- data/app/javascript/alchemy_admin/routes.js +18 -0
- data/app/javascript/alchemy_admin/turbo_stream_actions.js +88 -0
- data/app/javascript/alchemy_admin/utils/ajax.js +14 -3
- data/app/javascript/alchemy_admin/utils/events.js +5 -0
- data/app/javascript/alchemy_admin/utils/json_script.js +12 -0
- data/app/javascript/alchemy_admin/utils/tom_select.js +171 -0
- data/app/javascript/alchemy_admin.js +2 -19
- data/app/javascript/preview.js +131 -113
- data/app/jobs/alchemy/invalidate_elements_cache_job.rb +1 -1
- data/app/models/alchemy/admin/content_security_policy.rb +143 -0
- data/app/models/alchemy/admin/filters/base.rb +7 -0
- data/app/models/alchemy/admin/filters/datepicker.rb +1 -1
- data/app/models/alchemy/admin/filters/select.rb +20 -7
- data/app/models/alchemy/attachment.rb +14 -7
- data/app/models/alchemy/cache_control.rb +104 -0
- data/app/models/alchemy/current.rb +1 -1
- data/app/models/alchemy/eager_loading.rb +0 -2
- data/app/models/alchemy/element.rb +23 -8
- data/app/models/alchemy/element_definition.rb +1 -0
- data/app/models/alchemy/elements_repository.rb +11 -0
- data/app/models/alchemy/folded_page.rb +5 -1
- data/app/models/alchemy/ingredient.rb +26 -0
- data/app/models/alchemy/ingredients/datetime.rb +1 -1
- data/app/models/alchemy/ingredients/link.rb +2 -0
- data/app/models/alchemy/ingredients/picture.rb +3 -0
- data/app/models/alchemy/ingredients/text.rb +2 -0
- data/app/models/alchemy/language.rb +19 -7
- data/app/models/alchemy/legacy_page_url.rb +6 -1
- data/app/models/alchemy/node.rb +51 -0
- data/app/models/alchemy/page/page_natures.rb +41 -8
- data/app/models/alchemy/page/page_scopes.rb +23 -0
- data/app/models/alchemy/page/sitemap_cache_key.rb +56 -0
- data/app/models/alchemy/page.rb +88 -32
- data/app/models/alchemy/page_definition.rb +1 -1
- data/app/models/alchemy/page_mutex.rb +16 -0
- data/app/models/alchemy/page_version.rb +24 -1
- data/app/models/alchemy/permissions.rb +6 -5
- data/app/models/alchemy/picture.rb +15 -7
- data/app/models/alchemy/picture_description.rb +22 -0
- data/app/models/alchemy/picture_thumb.rb +18 -0
- data/app/models/alchemy/publishable/timestamp_resolver.rb +70 -0
- data/app/models/alchemy/resource.rb +1 -1
- data/app/models/alchemy/safe_url_validator.rb +34 -0
- data/app/models/alchemy/site.rb +8 -3
- data/app/models/alchemy/storage_adapter/active_storage/preprocessor.rb +7 -3
- data/app/models/alchemy/storage_adapter/active_storage.rb +4 -1
- data/app/models/alchemy/tag.rb +10 -4
- data/{lib → app/models/concerns}/alchemy/filetypes.rb +5 -0
- data/app/models/concerns/alchemy/picture_thumbnails.rb +61 -6
- data/app/models/concerns/alchemy/publishable.rb +14 -29
- data/app/models/concerns/alchemy/taggable.rb +92 -0
- data/app/models/concerns/alchemy/user_methods.rb +95 -0
- data/app/services/alchemy/duplicate_element.rb +5 -2
- data/app/services/alchemy/element_preloader.rb +1 -2
- data/app/stylesheets/alchemy/_custom-properties.scss +1 -0
- data/app/stylesheets/alchemy/_extends.scss +0 -8
- data/app/stylesheets/alchemy/_themes.scss +10 -9
- data/app/stylesheets/alchemy/admin/_empty-state.scss +29 -0
- data/app/stylesheets/alchemy/admin/_help.scss +10 -0
- data/app/stylesheets/alchemy/admin/_tom-select.scss +104 -15
- data/app/stylesheets/alchemy/admin/archive.scss +56 -11
- data/app/stylesheets/alchemy/admin/base.scss +4 -5
- data/app/stylesheets/alchemy/admin/buttons.scss +12 -1
- data/app/stylesheets/alchemy/admin/dashboard.scss +150 -20
- data/app/stylesheets/alchemy/admin/dialogs.scss +27 -36
- data/app/stylesheets/alchemy/admin/element-select.scss +1 -1
- data/app/stylesheets/alchemy/admin/elements.scss +8 -24
- data/app/stylesheets/alchemy/admin/flash.scss +2 -1
- data/app/stylesheets/alchemy/admin/forms.scss +60 -24
- data/app/stylesheets/alchemy/admin/frame.scss +40 -4
- data/app/stylesheets/alchemy/admin/image_library.scss +9 -12
- data/app/stylesheets/alchemy/admin/navigation.scss +17 -14
- data/app/stylesheets/alchemy/admin/node-select.scss +13 -37
- data/app/stylesheets/alchemy/admin/nodes.scss +1 -1
- data/app/stylesheets/alchemy/admin/page-select.scss +6 -97
- data/app/stylesheets/alchemy/admin/print.scss +0 -4
- data/app/stylesheets/alchemy/admin/remote-select.scss +170 -0
- data/app/stylesheets/alchemy/admin/search.scss +2 -3
- data/app/stylesheets/alchemy/admin/selects.scss +1 -294
- data/app/stylesheets/alchemy/admin/sitemap.scss +2 -1
- data/app/stylesheets/alchemy/admin/tables.scss +7 -1
- data/app/stylesheets/alchemy/admin/tags-autocomplete.scss +17 -0
- data/app/stylesheets/alchemy/admin/tags.scss +0 -13
- data/app/stylesheets/alchemy/admin/toolbar.scss +9 -6
- data/app/stylesheets/alchemy/admin/upload.scss +61 -1
- data/app/stylesheets/alchemy/admin.scss +4 -1
- data/app/stylesheets/tinymce/skins/ui/alchemy/skin.scss +19 -0
- data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +19 -0
- data/app/types/alchemy/cache_control_type.rb +84 -0
- data/app/views/alchemy/_edit_mode.html.erb +1 -1
- data/app/views/alchemy/_menubar.html.erb +2 -154
- data/app/views/alchemy/_preview_mode_code.html.erb +0 -3
- data/app/views/alchemy/admin/_left_menu.html.erb +18 -5
- data/app/views/alchemy/admin/_top_menu.html.erb +1 -1
- data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +5 -4
- data/app/views/alchemy/admin/attachments/_file_to_assign.html.erb +3 -3
- data/app/views/alchemy/admin/attachments/_files_list.html.erb +71 -83
- data/app/views/alchemy/admin/attachments/assign.turbo_stream.erb +8 -0
- data/app/views/alchemy/admin/attachments/index.html.erb +32 -7
- data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +1 -1
- data/app/views/alchemy/admin/crop.html.erb +20 -20
- data/app/views/alchemy/admin/dashboard/_footer.html.erb +0 -3
- data/app/views/alchemy/admin/dashboard/_top.html.erb +1 -0
- data/app/views/alchemy/admin/dashboard/help.html.erb +84 -57
- data/app/views/alchemy/admin/dashboard/license.html.erb +6 -0
- data/app/views/alchemy/admin/elements/_footer.html.erb +0 -1
- data/app/views/alchemy/admin/elements/_form.html.erb +1 -1
- data/app/views/alchemy/admin/elements/_header.html.erb +2 -19
- data/app/views/alchemy/admin/elements/_preview_text_quote.html.erb +3 -0
- data/app/views/alchemy/admin/elements/_schedule.html.erb +7 -7
- data/app/views/alchemy/admin/elements/_toolbar.html.erb +1 -1
- data/app/views/alchemy/admin/elements/create.turbo_stream.erb +1 -1
- data/app/views/alchemy/admin/elements/index.html.erb +1 -1
- data/app/views/alchemy/admin/elements/publish.turbo_stream.erb +2 -2
- data/app/views/alchemy/admin/elements/update.turbo_stream.erb +31 -0
- data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +6 -3
- data/app/views/alchemy/admin/languages/_table.html.erb +0 -2
- data/app/views/alchemy/admin/languages/index.html.erb +25 -29
- data/app/views/alchemy/admin/layoutpages/_layoutpage.html.erb +1 -3
- data/app/views/alchemy/admin/layoutpages/edit.html.erb +1 -1
- data/app/views/alchemy/admin/layoutpages/index.html.erb +26 -7
- data/app/views/alchemy/admin/nodes/_form.html.erb +3 -21
- data/app/views/alchemy/admin/nodes/_node.html.erb +93 -88
- data/app/views/alchemy/admin/nodes/index.html.erb +15 -23
- data/app/views/alchemy/admin/pages/_create_language_form.html.erb +2 -6
- data/app/views/alchemy/admin/pages/_form.html.erb +2 -1
- data/app/views/alchemy/admin/pages/_publication_fields.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_table.html.erb +2 -4
- data/app/views/alchemy/admin/pages/_toolbar.html.erb +2 -1
- data/app/views/alchemy/admin/pages/edit.html.erb +10 -21
- data/app/views/alchemy/admin/pages/flush.turbo_stream.erb +1 -1
- data/app/views/alchemy/admin/pages/index.html.erb +26 -31
- data/app/views/alchemy/admin/pages/info.html.erb +2 -2
- data/app/views/alchemy/admin/pages/update.turbo_stream.erb +2 -2
- data/app/views/alchemy/admin/partials/_language_tree_select.html.erb +1 -1
- data/app/views/alchemy/admin/partials/_routes.html.erb +24 -48
- data/app/views/alchemy/admin/pictures/_archive.html.erb +3 -17
- data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +5 -3
- data/app/views/alchemy/admin/pictures/_form.html.erb +0 -1
- data/app/views/alchemy/admin/pictures/_infos.html.erb +5 -1
- data/app/views/alchemy/admin/pictures/_picture.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +3 -4
- data/app/views/alchemy/admin/pictures/assign.turbo_stream.erb +7 -0
- data/app/views/alchemy/admin/pictures/index.html.erb +38 -30
- data/app/views/alchemy/admin/pictures/show.html.erb +41 -41
- data/app/views/alchemy/admin/resources/_empty_state.html.erb +27 -0
- data/app/views/alchemy/admin/resources/_pagination.html.erb +1 -1
- data/app/views/alchemy/admin/resources/_per_page_select.html.erb +1 -1
- data/app/views/alchemy/admin/resources/_resource_table.html.erb +0 -2
- data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +3 -2
- data/app/views/alchemy/admin/resources/index.html.erb +32 -13
- data/app/views/alchemy/admin/sites/index.html.erb +24 -22
- data/app/views/alchemy/admin/styleguide/index.html.erb +35 -12
- data/app/views/alchemy/admin/tags/index.html.erb +27 -25
- data/app/views/alchemy/admin/tinymce/_setup.html.erb +6 -8
- data/app/views/alchemy/admin/translations/_en.js +3 -0
- data/app/views/alchemy/admin/uploader/_button.html.erb +11 -17
- data/app/views/alchemy/admin/uploader/_setup.html.erb +3 -3
- data/app/views/alchemy/pages/sitemap.xml.erb +2 -0
- data/app/views/kaminari/alchemy/_first_page.html.erb +1 -2
- data/app/views/kaminari/alchemy/_gap.html.erb +0 -1
- data/app/views/kaminari/alchemy/_last_page.html.erb +1 -2
- data/app/views/kaminari/alchemy/_next_page.html.erb +1 -2
- data/app/views/kaminari/alchemy/_page.html.erb +1 -2
- data/app/views/kaminari/alchemy/_paginator.html.erb +0 -1
- data/app/views/kaminari/alchemy/_prev_page.html.erb +1 -2
- data/app/views/layouts/alchemy/admin/dialog.html.erb +3 -0
- data/app/views/layouts/alchemy/admin.html.erb +5 -6
- data/config/brakeman.ignore +23 -1
- data/config/importmap.rb +10 -8
- data/config/initializers/dragonfly.rb +21 -17
- data/config/locales/alchemy.en.yml +53 -12
- data/config/routes.rb +3 -3
- data/db/migrate/20260727120000_add_permitted_roles_to_alchemy_pages.rb +23 -0
- data/db/migrate/20260820120000_add_cached_tag_list_to_taggables.rb +57 -0
- data/lib/alchemy/configuration/number_option.rb +13 -0
- data/lib/alchemy/configuration/proc_option.rb +13 -0
- data/lib/alchemy/configuration.rb +2 -0
- data/lib/alchemy/configurations/admin_components.rb +30 -0
- data/lib/alchemy/configurations/format_matchers.rb +7 -1
- data/lib/alchemy/configurations/main.rb +103 -4
- data/lib/alchemy/configurations/menu.rb +91 -0
- data/lib/alchemy/configurations/menu_item.rb +123 -0
- data/lib/alchemy/configurations/sitemap.rb +1 -0
- data/lib/alchemy/configurations/uploader.rb +15 -1
- data/lib/alchemy/engine.rb +0 -6
- data/lib/alchemy/i18n.rb +26 -0
- data/lib/alchemy/modules.rb +52 -50
- data/lib/alchemy/tasks/generate_thumbnails.rb +99 -0
- data/lib/alchemy/tasks/tidy.rb +1 -1
- data/lib/alchemy/test_support/capybara_helpers.rb +48 -37
- data/lib/alchemy/test_support/current_language_shared_examples.rb +1 -1
- data/lib/alchemy/test_support/factories/dummy_user_factory.rb +4 -1
- data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +132 -0
- data/lib/alchemy/test_support/shared_ingredient_examples.rb +52 -0
- data/lib/alchemy/test_support/shared_publishable_examples.rb +39 -4
- data/lib/alchemy/upgrader/eight_four.rb +237 -0
- data/lib/alchemy/upgrader/register_module_rewriter.rb +177 -0
- data/lib/alchemy/upgrader.rb +2 -1
- data/lib/alchemy/version.rb +1 -1
- data/lib/alchemy.rb +0 -1
- data/lib/alchemy_cms.rb +0 -13
- data/lib/generators/alchemy/elements/templates/view.html.erb +1 -1
- data/lib/generators/alchemy/elements/templates/view.html.haml +1 -1
- data/lib/generators/alchemy/elements/templates/view.html.slim +1 -1
- data/lib/generators/alchemy/install/install_generator.rb +2 -13
- data/lib/generators/alchemy/install/templates/alchemy.rb.tt +24 -9
- data/lib/generators/alchemy/module/module_generator.rb +1 -0
- data/lib/generators/alchemy/module/templates/ability.rb.tt +1 -1
- data/lib/generators/alchemy/module/templates/module_config.rb.tt +16 -18
- data/lib/generators/alchemy/user_columns_migration/templates/migration.rb.tt +19 -0
- data/lib/generators/alchemy/user_columns_migration/user_columns_migration_generator.rb +30 -0
- data/lib/tasks/alchemy/thumbnails.rake +16 -29
- data/lib/tasks/alchemy/upgrade.rake +45 -3
- data/vendor/javascript/cropperjs.min.js +3 -3
- data/vendor/javascript/floating-ui.min.js +1 -1
- data/vendor/javascript/tinymce.min.js +5 -5
- data/vendor/javascript/tom-select.min.js +1 -1
- metadata +85 -65
- data/app/javascript/alchemy_admin/components/action.js +0 -50
- data/app/javascript/alchemy_admin/dirty.js +0 -55
- data/app/javascript/alchemy_admin/node_tree.js +0 -82
- data/app/javascript/alchemy_admin/picture_selector.js +0 -67
- data/app/javascript/alchemy_admin/templates/compiled.js +0 -1
- data/app/javascript/alchemy_admin/templates/node_folder.hbs +0 -3
- data/app/stylesheets/alchemy/admin/attachment-select.scss +0 -19
- data/app/views/alchemy/admin/_main_navi.html.erb +0 -6
- data/app/views/alchemy/admin/attachments/assign.js.erb +0 -15
- data/app/views/alchemy/admin/pages/_page_layout_filter.html.erb +0 -30
- data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +0 -20
- data/app/views/alchemy/admin/partials/_sub_navigation.html.erb +0 -11
- data/app/views/alchemy/admin/pictures/assign.js.erb +0 -13
- data/app/views/alchemy/base/error_notice.js.erb +0 -1
- data/app/views/alchemy/base/permission_denied.js.erb +0 -3
- data/app/views/alchemy/base/redirect.js.erb +0 -11
- data/config/alchemy/config.yml +0 -217
- data/config/alchemy/modules.yml +0 -82
- data/config/initializers/rails_live_reload.rb +0 -13
- data/lib/alchemy/dev_support/live_reload_watcher.rb +0 -5
- data/lib/alchemy/taggable.rb +0 -49
- data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +0 -34
- data/vendor/assets/stylesheets/alchemy_admin/select2.css +0 -1
- data/vendor/javascript/handlebars.min.js +0 -29
- data/vendor/javascript/jquery.min.js +0 -2
- data/vendor/javascript/rails-ujs.min.js +0 -1
- data/vendor/javascript/select2.min.js +0 -23
- /data/{lib → app/controllers/concerns}/alchemy/ability_helper.rb +0 -0
- /data/{lib → app/controllers/concerns}/alchemy/admin/locale.rb +0 -0
- /data/{lib → app/controllers/concerns}/alchemy/admin/preview_time.rb +0 -0
- /data/{lib → app/controllers/concerns}/alchemy/admin/timezone.rb +0 -0
- /data/{lib → app/controllers/concerns}/alchemy/configuration_methods.rb +0 -0
- /data/{lib → app/models}/alchemy/admin/preview_url.rb +0 -0
- /data/{lib → app/models/concerns}/alchemy/hints.rb +0 -0
- /data/{lib → app/models/concerns}/alchemy/name_conversions.rb +0 -0
- /data/{lib → app/services}/alchemy/elements_finder.rb +0 -0
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
import { Turbo } from "@hotwired/turbo-rails"
|
|
1
2
|
import Hotkeys from "alchemy_admin/hotkeys"
|
|
2
3
|
import Spinner from "alchemy_admin/spinner"
|
|
4
|
+
import { createHtmlElement } from "alchemy_admin/utils/dom_helpers"
|
|
5
|
+
import { dispatchCustomEvent } from "alchemy_admin/utils/events"
|
|
3
6
|
|
|
4
7
|
// Collection of all current dialog instances
|
|
5
8
|
const currentDialogs = []
|
|
6
9
|
|
|
10
|
+
// Id of the Turbo Frame wrapping the dialog content.
|
|
11
|
+
// Keep in sync with Alchemy::Admin::DIALOG_FRAME_ID.
|
|
12
|
+
export const DIALOG_FRAME_ID = "alchemy_dialog_frame"
|
|
13
|
+
|
|
14
|
+
const TURBO_STREAM_CONTENT_TYPE = "text/vnd.turbo-stream.html"
|
|
15
|
+
|
|
7
16
|
const DEFAULTS = {
|
|
8
17
|
header_height: 36,
|
|
9
18
|
size: "400x300",
|
|
@@ -16,6 +25,9 @@ const DEFAULTS = {
|
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
export class Dialog {
|
|
28
|
+
#previousFlashParent = null
|
|
29
|
+
#closing = false
|
|
30
|
+
|
|
19
31
|
// Arguments:
|
|
20
32
|
// - url: The url to load the content from via ajax
|
|
21
33
|
// - options: A object holding options
|
|
@@ -24,9 +36,6 @@ export class Dialog {
|
|
|
24
36
|
constructor(url, options = {}) {
|
|
25
37
|
this.url = url
|
|
26
38
|
this.options = { ...DEFAULTS, ...options }
|
|
27
|
-
this.$document = $(document)
|
|
28
|
-
this.$window = $(window)
|
|
29
|
-
this.$body = $("body")
|
|
30
39
|
const size = this.options.size.split("x")
|
|
31
40
|
this.width = parseInt(size[0], 10)
|
|
32
41
|
this.height = parseInt(size[1], 10)
|
|
@@ -36,85 +45,153 @@ export class Dialog {
|
|
|
36
45
|
|
|
37
46
|
// Opens the Dialog and loads the content via ajax.
|
|
38
47
|
open() {
|
|
39
|
-
this.dialog
|
|
48
|
+
dispatchCustomEvent(this.dialog, "Alchemy.DialogOpen")
|
|
40
49
|
this.bind_close_events()
|
|
50
|
+
if (this.options.modal) {
|
|
51
|
+
this.dialog_container.showModal()
|
|
52
|
+
this.#adoptFlashNotices()
|
|
53
|
+
} else {
|
|
54
|
+
this.dialog_container.show()
|
|
55
|
+
}
|
|
41
56
|
window.requestAnimationFrame(() => {
|
|
42
|
-
this.dialog_container.
|
|
43
|
-
if (this.overlay != null) {
|
|
44
|
-
return this.overlay.addClass("open")
|
|
45
|
-
}
|
|
57
|
+
this.dialog_container.classList.add("open")
|
|
46
58
|
})
|
|
47
|
-
|
|
59
|
+
document.body.classList.add("prevent-scrolling")
|
|
48
60
|
currentDialogs.push(this)
|
|
49
61
|
this.load()
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
// Closes the Dialog and removes it from the DOM
|
|
53
65
|
close() {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
// The container stays open during the closing transition, so close() can be
|
|
67
|
+
// called again mid-fade (mashing Esc, a double click on the close button or
|
|
68
|
+
// backdrop). Guard against re-entry, otherwise a second transitionend
|
|
69
|
+
// listener is registered and the teardown runs twice.
|
|
70
|
+
if (this.#closing) {
|
|
71
|
+
return true
|
|
59
72
|
}
|
|
60
|
-
this
|
|
61
|
-
|
|
73
|
+
this.#closing = true
|
|
74
|
+
dispatchCustomEvent(this.dialog, "DialogClose.Alchemy")
|
|
75
|
+
this.dialog_container.classList.remove("open")
|
|
76
|
+
this.dialog_container.addEventListener(
|
|
77
|
+
"transitionend",
|
|
62
78
|
() => {
|
|
63
|
-
|
|
79
|
+
// Move the flash notices back out before removing the container, so
|
|
80
|
+
// they are not destroyed together with the dialog.
|
|
81
|
+
this.#releaseFlashNotices()
|
|
82
|
+
this.dialog_container.close()
|
|
64
83
|
this.dialog_container.remove()
|
|
65
|
-
|
|
66
|
-
|
|
84
|
+
currentDialogs.pop()
|
|
85
|
+
if (currentDialogs.length === 0) {
|
|
86
|
+
document.body.classList.remove("prevent-scrolling")
|
|
67
87
|
}
|
|
68
|
-
this.$body.removeClass("prevent-scrolling")
|
|
69
|
-
currentDialogs.pop(this)
|
|
70
88
|
if (this.options.closed != null) {
|
|
71
|
-
|
|
89
|
+
this.options.closed()
|
|
72
90
|
}
|
|
73
|
-
}
|
|
91
|
+
},
|
|
92
|
+
{ once: true }
|
|
74
93
|
)
|
|
75
94
|
return true
|
|
76
95
|
}
|
|
77
96
|
|
|
97
|
+
// A modal <dialog> makes everything outside its subtree inert, so flash
|
|
98
|
+
// notices rendered on the body would be visible but not interactable while
|
|
99
|
+
// the dialog is open. Move them into the dialog, which is in the top layer
|
|
100
|
+
// and not inert, for the dialog's lifetime and restore them on close.
|
|
101
|
+
#adoptFlashNotices() {
|
|
102
|
+
const flashNotices = document.getElementById("flash_notices")
|
|
103
|
+
if (flashNotices) {
|
|
104
|
+
this.#previousFlashParent = flashNotices.parentElement
|
|
105
|
+
this.dialog_container.append(flashNotices)
|
|
106
|
+
this.#clearTransientNotices(flashNotices)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The notices are not cleared on the way back. Anything transient left at
|
|
111
|
+
// this point was raised by the dialog itself, typically the growl of the
|
|
112
|
+
// response that closes it, and has to outlive the dialog. Moving it re-arms
|
|
113
|
+
// its dismiss timer, which is what a just-raised notice wants anyway.
|
|
114
|
+
#releaseFlashNotices() {
|
|
115
|
+
const flashNotices = document.getElementById("flash_notices")
|
|
116
|
+
if (flashNotices && this.#previousFlashParent) {
|
|
117
|
+
this.#previousFlashParent.append(flashNotices)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Only the persistent error notices need to travel with the dialog to stay
|
|
122
|
+
// interactable. Moving the auto-dismissing ones would restart their dismiss
|
|
123
|
+
// timers (their connectedCallback re-arms the timer), making them linger, so
|
|
124
|
+
// remove them instead. A fade-out is skipped on purpose: the just-moved
|
|
125
|
+
// element has no transition baseline, so dismiss()'s transitionend would never
|
|
126
|
+
// fire and the node would be stranded invisible-but-present.
|
|
127
|
+
#clearTransientNotices(flashNotices) {
|
|
128
|
+
flashNotices
|
|
129
|
+
.querySelectorAll('alchemy-message[dismissable]:not([type="error"])')
|
|
130
|
+
.forEach((message) => message.remove())
|
|
131
|
+
}
|
|
132
|
+
|
|
78
133
|
// Loads the content via ajax and replaces the Dialog body with server response.
|
|
134
|
+
//
|
|
135
|
+
// The Turbo-Frame header tells the server this content is rendered for a
|
|
136
|
+
// dialog, so it wraps the response in the dialog frame (see set_layout). The
|
|
137
|
+
// same action requested as a full page is not wrapped.
|
|
79
138
|
load() {
|
|
80
139
|
this.show_spinner()
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}).fail((xhr) => {
|
|
84
|
-
this.show_error(xhr)
|
|
140
|
+
fetch(this.url, {
|
|
141
|
+
headers: { "Turbo-Frame": DIALOG_FRAME_ID }
|
|
85
142
|
})
|
|
143
|
+
.then(async (response) => {
|
|
144
|
+
const responseText = await response.text()
|
|
145
|
+
// A stream answers a request the frame must not render, whatever the
|
|
146
|
+
// status: an expired session sends a `dialog_visit` to the login page.
|
|
147
|
+
// Without this the stream is never run and the dialog shows a 401 the
|
|
148
|
+
// user cannot act on.
|
|
149
|
+
if (
|
|
150
|
+
response.headers
|
|
151
|
+
.get("content-type")
|
|
152
|
+
?.includes(TURBO_STREAM_CONTENT_TYPE)
|
|
153
|
+
) {
|
|
154
|
+
Turbo.renderStreamMessage(responseText)
|
|
155
|
+
} else if (response.ok) {
|
|
156
|
+
this.replace(responseText)
|
|
157
|
+
} else {
|
|
158
|
+
this.show_error({
|
|
159
|
+
status: response.status,
|
|
160
|
+
statusText: response.statusText,
|
|
161
|
+
responseText
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
.catch(() => {
|
|
166
|
+
this.show_error({ status: 0 })
|
|
167
|
+
})
|
|
86
168
|
}
|
|
87
169
|
|
|
88
170
|
// Reloads the Dialog content
|
|
89
171
|
reload() {
|
|
90
|
-
this.dialog_body.
|
|
172
|
+
this.dialog_body.innerHTML = ""
|
|
91
173
|
this.load()
|
|
92
174
|
}
|
|
93
175
|
|
|
94
176
|
// Replaces the dialog body with given content and initializes it.
|
|
95
177
|
replace(data) {
|
|
96
178
|
this.remove_spinner()
|
|
97
|
-
this.dialog_body.
|
|
98
|
-
this.dialog_body.
|
|
179
|
+
this.dialog_body.style.display = "none"
|
|
180
|
+
this.dialog_body.innerHTML = data
|
|
99
181
|
this.init()
|
|
100
|
-
this.dialog
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
detail: {
|
|
104
|
-
body: this.dialog_body[0]
|
|
105
|
-
}
|
|
106
|
-
})
|
|
107
|
-
)
|
|
182
|
+
dispatchCustomEvent(this.dialog, "DialogReady.Alchemy", {
|
|
183
|
+
body: this.dialog_body
|
|
184
|
+
})
|
|
108
185
|
if (this.options.ready != null) {
|
|
109
186
|
this.options.ready(this.dialog_body)
|
|
110
187
|
}
|
|
111
|
-
this.dialog_body.
|
|
188
|
+
this.dialog_body.style.display = ""
|
|
112
189
|
}
|
|
113
190
|
|
|
114
191
|
// Adds a spinner into Dialog body
|
|
115
192
|
show_spinner() {
|
|
116
193
|
this.spinner = new Spinner("medium")
|
|
117
|
-
this.spinner.spin(this.dialog_body
|
|
194
|
+
this.spinner.spin(this.dialog_body)
|
|
118
195
|
}
|
|
119
196
|
|
|
120
197
|
// Removes the spinner from Dialog body
|
|
@@ -125,38 +202,27 @@ export class Dialog {
|
|
|
125
202
|
// Initializes the Dialog body
|
|
126
203
|
init() {
|
|
127
204
|
Hotkeys(this.dialog_body)
|
|
128
|
-
|
|
129
|
-
window.requestAnimationFrame(() => {
|
|
130
|
-
this.dialog_body.find("[autofocus]").focus()
|
|
131
|
-
})
|
|
205
|
+
window.requestAnimationFrame(() => this.#focusInitialElement())
|
|
132
206
|
}
|
|
133
207
|
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
$form.on("ajax:error", (event) => {
|
|
150
|
-
const statusText = event.detail[1]
|
|
151
|
-
const xhr = event.detail[2]
|
|
152
|
-
this.show_error(xhr, statusText)
|
|
153
|
-
})
|
|
208
|
+
// Shoelace tab panels (link, page configure) only render a few frames after
|
|
209
|
+
// the content loaded, so their fields can not take focus yet. Hence the retry.
|
|
210
|
+
#focusInitialElement(attempts = 0) {
|
|
211
|
+
const target =
|
|
212
|
+
this.dialog_body.querySelector("[autofocus]") ??
|
|
213
|
+
this.dialog_body.querySelector("form [type='submit']")
|
|
214
|
+
target?.focus()
|
|
215
|
+
if (document.activeElement === this.close_button && attempts < 20) {
|
|
216
|
+
window.requestAnimationFrame(() =>
|
|
217
|
+
this.#focusInitialElement(attempts + 1)
|
|
218
|
+
)
|
|
219
|
+
}
|
|
154
220
|
}
|
|
155
221
|
|
|
156
222
|
// Displays an error message
|
|
157
223
|
show_error(xhr, statusText) {
|
|
158
224
|
if (xhr.status === 422) {
|
|
159
|
-
this.dialog_body.
|
|
225
|
+
this.dialog_body.innerHTML = xhr.responseText
|
|
160
226
|
this.init()
|
|
161
227
|
return
|
|
162
228
|
}
|
|
@@ -166,12 +232,10 @@ export class Dialog {
|
|
|
166
232
|
statusText
|
|
167
233
|
)
|
|
168
234
|
|
|
169
|
-
|
|
235
|
+
this.dialog_body.innerHTML = `<alchemy-message type="${error_type}">
|
|
170
236
|
<h1>${error_header}</h1>
|
|
171
237
|
<p>${error_body}</p>
|
|
172
|
-
</alchemy-message>`
|
|
173
|
-
|
|
174
|
-
this.dialog_body.html($errorDiv)
|
|
238
|
+
</alchemy-message>`
|
|
175
239
|
}
|
|
176
240
|
|
|
177
241
|
// Returns error message based on xhr status
|
|
@@ -206,55 +270,69 @@ export class Dialog {
|
|
|
206
270
|
// Binds close events on:
|
|
207
271
|
// - Close button
|
|
208
272
|
// - Overlay (if the Dialog is a modal)
|
|
209
|
-
// - ESC Key
|
|
273
|
+
// - ESC Key (the dialog element's cancel event)
|
|
210
274
|
bind_close_events() {
|
|
211
|
-
this.close_button.
|
|
275
|
+
this.close_button.addEventListener("click", (e) => {
|
|
276
|
+
e.preventDefault()
|
|
212
277
|
this.close()
|
|
213
278
|
})
|
|
214
|
-
this.dialog_container.
|
|
215
|
-
|
|
216
|
-
return true
|
|
217
|
-
}
|
|
279
|
+
this.dialog_container.addEventListener("close", (e) => {
|
|
280
|
+
e.preventDefault()
|
|
218
281
|
this.close()
|
|
219
|
-
return false
|
|
220
282
|
})
|
|
221
|
-
this
|
|
222
|
-
|
|
283
|
+
this.dialog_container.classList.add("closable")
|
|
284
|
+
// Use pointerdown, not click: a click whose mousedown and mouseup land on
|
|
285
|
+
// different nodes is dispatched to their common ancestor — this element for
|
|
286
|
+
// anything inside the dialog — closing it although the backdrop was never
|
|
287
|
+
// hit, e.g. when selecting text and releasing outside.
|
|
288
|
+
this.dialog_container.addEventListener("pointerdown", (e) => {
|
|
289
|
+
if (e.target === this.dialog_container) {
|
|
223
290
|
this.close()
|
|
224
|
-
return false
|
|
225
|
-
} else {
|
|
226
|
-
return true
|
|
227
291
|
}
|
|
228
292
|
})
|
|
293
|
+
this.dialog_container.addEventListener("cancel", (e) => {
|
|
294
|
+
// The <dialog>'s own cancel event (Esc) targets the container, but a file
|
|
295
|
+
// input's native cancel event (dismissing the OS file picker) bubbles up
|
|
296
|
+
// to the same node. Only the former should close the dialog.
|
|
297
|
+
if (e.target !== this.dialog_container) {
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
e.preventDefault()
|
|
301
|
+
this.close()
|
|
302
|
+
})
|
|
229
303
|
}
|
|
230
304
|
|
|
231
305
|
// Builds the html structure of the Dialog
|
|
232
306
|
build() {
|
|
233
|
-
this.dialog_container =
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
this.
|
|
237
|
-
this.
|
|
238
|
-
|
|
307
|
+
this.dialog_container = createHtmlElement(
|
|
308
|
+
'<dialog class="alchemy-dialog-container"></dialog>'
|
|
309
|
+
)
|
|
310
|
+
this.dialog = createHtmlElement('<div class="alchemy-dialog"></div>')
|
|
311
|
+
this.dialog_body = createHtmlElement(
|
|
312
|
+
'<div class="alchemy-dialog-body"></div>'
|
|
313
|
+
)
|
|
314
|
+
this.dialog_header = createHtmlElement(
|
|
315
|
+
'<div class="alchemy-dialog-header"></div>'
|
|
316
|
+
)
|
|
317
|
+
this.dialog_title = createHtmlElement(
|
|
318
|
+
'<div class="alchemy-dialog-title"></div>'
|
|
319
|
+
)
|
|
320
|
+
this.close_button = createHtmlElement(
|
|
239
321
|
'<button class="alchemy-dialog-close"><alchemy-icon name="close"></alchemy-icon></button>'
|
|
240
322
|
)
|
|
241
|
-
this.dialog_title.
|
|
323
|
+
this.dialog_title.textContent = this.options.title
|
|
242
324
|
this.dialog_header.append(this.dialog_title)
|
|
243
325
|
this.dialog_header.append(this.close_button)
|
|
244
326
|
this.dialog.append(this.dialog_header)
|
|
245
327
|
this.dialog.append(this.dialog_body)
|
|
246
328
|
this.dialog_container.append(this.dialog)
|
|
247
329
|
if (this.options.modal) {
|
|
248
|
-
this.dialog.
|
|
330
|
+
this.dialog.classList.add("modal")
|
|
249
331
|
}
|
|
250
332
|
if (this.options.padding) {
|
|
251
|
-
this.dialog_body.
|
|
252
|
-
}
|
|
253
|
-
if (this.options.modal) {
|
|
254
|
-
this.overlay = $('<div class="alchemy-dialog-overlay" />')
|
|
255
|
-
this.$body.append(this.overlay)
|
|
333
|
+
this.dialog_body.classList.add("padded")
|
|
256
334
|
}
|
|
257
|
-
|
|
335
|
+
document.body.append(this.dialog_container)
|
|
258
336
|
}
|
|
259
337
|
|
|
260
338
|
// Sets the correct size of the dialog
|
|
@@ -262,29 +340,23 @@ export class Dialog {
|
|
|
262
340
|
resize() {
|
|
263
341
|
const { width, height } = this.getSize()
|
|
264
342
|
|
|
265
|
-
this.dialog.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
overflow: this.options.overflow
|
|
269
|
-
})
|
|
343
|
+
this.dialog.style.width = `${width}px`
|
|
344
|
+
this.dialog.style.minHeight = `${height}px`
|
|
345
|
+
this.dialog.style.overflow = this.options.overflow
|
|
270
346
|
|
|
271
347
|
if (this.options.overflow === "hidden") {
|
|
272
|
-
this.dialog_body.
|
|
273
|
-
|
|
274
|
-
overflow: "auto"
|
|
275
|
-
})
|
|
348
|
+
this.dialog_body.style.height = `${height}px`
|
|
349
|
+
this.dialog_body.style.overflow = "auto"
|
|
276
350
|
} else {
|
|
277
|
-
this.dialog_body.
|
|
278
|
-
|
|
279
|
-
overflow: "visible"
|
|
280
|
-
})
|
|
351
|
+
this.dialog_body.style.minHeight = `${height}px`
|
|
352
|
+
this.dialog_body.style.overflow = "visible"
|
|
281
353
|
}
|
|
282
354
|
}
|
|
283
355
|
|
|
284
356
|
getSize() {
|
|
285
357
|
const padding = this.options.padding ? 16 : 0
|
|
286
|
-
const doc_width =
|
|
287
|
-
const doc_height =
|
|
358
|
+
const doc_width = window.innerWidth
|
|
359
|
+
const doc_height = window.innerHeight
|
|
288
360
|
|
|
289
361
|
let width = this.width
|
|
290
362
|
let height = this.height
|
|
@@ -3,11 +3,6 @@ import "keymaster"
|
|
|
3
3
|
const bindedHotkeys = []
|
|
4
4
|
|
|
5
5
|
export default function (scope = document) {
|
|
6
|
-
// The scope can be a jQuery object because we still use jQuery in alchemy_admin/dialog.js.
|
|
7
|
-
if (scope instanceof jQuery) {
|
|
8
|
-
scope = scope[0]
|
|
9
|
-
}
|
|
10
|
-
|
|
11
6
|
// Unbind all previously registered hotkeys if we are not inside a dialog.
|
|
12
7
|
if (scope === document) {
|
|
13
8
|
bindedHotkeys.forEach((hotkey) => key.unbind(hotkey))
|
|
@@ -11,7 +11,9 @@ function nestedTranslation(translations, key) {
|
|
|
11
11
|
|
|
12
12
|
function getTranslation(key) {
|
|
13
13
|
const locale = currentLocale()
|
|
14
|
-
|
|
14
|
+
// Host engines import components standalone, outside of the admin layout
|
|
15
|
+
// that defines the global.
|
|
16
|
+
const translations = window.Alchemy?.translations
|
|
15
17
|
|
|
16
18
|
if (!translations) {
|
|
17
19
|
console.warn(`Translations for locale ${locale} not found!`)
|
|
@@ -39,11 +41,3 @@ export function translate(key, replacement = undefined) {
|
|
|
39
41
|
}
|
|
40
42
|
return translation
|
|
41
43
|
}
|
|
42
|
-
|
|
43
|
-
export async function setupSelectLocale() {
|
|
44
|
-
const locale = currentLocale()
|
|
45
|
-
if (locale === "en") return
|
|
46
|
-
|
|
47
|
-
await import(`select2/${locale}.js`)
|
|
48
|
-
$.extend($.fn.select2.defaults, $.fn.select2.locales[locale])
|
|
49
|
-
}
|
|
@@ -1,71 +1,87 @@
|
|
|
1
1
|
import { Dialog } from "alchemy_admin/dialog"
|
|
2
|
+
import { createHtmlElement } from "alchemy_admin/utils/dom_helpers"
|
|
2
3
|
|
|
3
4
|
export default class ImageOverlay extends Dialog {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
// The picture is rendered in a Turbo frame that replaces its content on every
|
|
6
|
+
// navigation, so the picture elements are looked up on demand instead of being
|
|
7
|
+
// bound once.
|
|
8
|
+
#clickHandler = (e) => {
|
|
9
|
+
if (e.target.closest(".picture-overlay-handle")) {
|
|
10
|
+
e.preventDefault()
|
|
11
|
+
this.dialog.classList.toggle("hide-form")
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
if (e.target.closest(".zoomed-picture-background")) {
|
|
10
15
|
e.stopPropagation()
|
|
11
16
|
if (e.target.nodeName === "IMG") {
|
|
12
17
|
return
|
|
13
18
|
}
|
|
14
19
|
this.close()
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#keydownHandler = (e) => {
|
|
24
|
+
if (e.target.nodeName === "INPUT" || e.target.nodeName === "TEXTAREA") {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
switch (e.key) {
|
|
28
|
+
case "ArrowLeft":
|
|
29
|
+
this.previous()
|
|
30
|
+
e.preventDefault()
|
|
31
|
+
break
|
|
32
|
+
case "ArrowRight":
|
|
33
|
+
this.next()
|
|
34
|
+
e.preventDefault()
|
|
35
|
+
break
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The overlay always fills the viewport via its stylesheet, so the inline
|
|
40
|
+
// dimensions the generic dialog applies would only cap it.
|
|
41
|
+
resize() {}
|
|
42
|
+
|
|
43
|
+
init() {
|
|
44
|
+
this.dialog_body.removeEventListener("click", this.#clickHandler)
|
|
45
|
+
this.dialog_body.addEventListener("click", this.#clickHandler)
|
|
23
46
|
this.#initKeyboardNavigation()
|
|
24
47
|
super.init()
|
|
25
48
|
}
|
|
26
49
|
|
|
27
50
|
previous() {
|
|
28
|
-
|
|
29
|
-
this.$previous[0].click()
|
|
30
|
-
}
|
|
51
|
+
this.dialog_body.querySelector(".previous-picture")?.click()
|
|
31
52
|
}
|
|
32
53
|
|
|
33
54
|
next() {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
55
|
+
this.dialog_body.querySelector(".next-picture")?.click()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
close() {
|
|
59
|
+
document.removeEventListener("keydown", this.#keydownHandler)
|
|
60
|
+
return super.close()
|
|
37
61
|
}
|
|
38
62
|
|
|
39
63
|
build() {
|
|
40
|
-
this.dialog_container =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
this.
|
|
64
|
+
this.dialog_container = createHtmlElement(
|
|
65
|
+
'<dialog class="alchemy-image-overlay-container"></dialog>'
|
|
66
|
+
)
|
|
67
|
+
this.dialog = createHtmlElement(
|
|
68
|
+
'<div class="alchemy-image-overlay-dialog"></div>'
|
|
69
|
+
)
|
|
70
|
+
this.dialog_body = createHtmlElement(
|
|
71
|
+
'<div class="alchemy-image-overlay-body"></div>'
|
|
72
|
+
)
|
|
73
|
+
this.close_button =
|
|
74
|
+
createHtmlElement(`<a class="alchemy-image-overlay-close">
|
|
44
75
|
<alchemy-icon name="close" size="xl"></alchemy-icon>
|
|
45
76
|
</a>`)
|
|
46
77
|
this.dialog.append(this.close_button)
|
|
47
78
|
this.dialog.append(this.dialog_body)
|
|
48
79
|
this.dialog_container.append(this.dialog)
|
|
49
|
-
this.
|
|
50
|
-
this.$body.append(this.overlay)
|
|
51
|
-
this.$body.append(this.dialog_container)
|
|
80
|
+
document.body.append(this.dialog_container)
|
|
52
81
|
}
|
|
53
82
|
|
|
54
83
|
#initKeyboardNavigation() {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return true
|
|
58
|
-
}
|
|
59
|
-
switch (e.which) {
|
|
60
|
-
case 37:
|
|
61
|
-
this.previous()
|
|
62
|
-
return false
|
|
63
|
-
case 39:
|
|
64
|
-
this.next()
|
|
65
|
-
return false
|
|
66
|
-
default:
|
|
67
|
-
return true
|
|
68
|
-
}
|
|
69
|
-
})
|
|
84
|
+
document.removeEventListener("keydown", this.#keydownHandler)
|
|
85
|
+
document.addEventListener("keydown", this.#keydownHandler)
|
|
70
86
|
}
|
|
71
87
|
}
|
|
@@ -5,12 +5,12 @@ import { openDialog } from "alchemy_admin/dialog"
|
|
|
5
5
|
// Opens the help dialog when the user presses the "?" key outside of a field.
|
|
6
6
|
function showHelp(evt) {
|
|
7
7
|
if (
|
|
8
|
-
|
|
8
|
+
!evt.target.matches("input, textarea") &&
|
|
9
9
|
String.fromCharCode(evt.which) === "?"
|
|
10
10
|
) {
|
|
11
11
|
openDialog("/admin/help", {
|
|
12
12
|
title: Alchemy.t("help"),
|
|
13
|
-
size: "
|
|
13
|
+
size: "500x400"
|
|
14
14
|
})
|
|
15
15
|
return false
|
|
16
16
|
} else {
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { translate } from "alchemy_admin/i18n"
|
|
2
2
|
import { Dialog } from "alchemy_admin/dialog"
|
|
3
|
+
import { routes } from "alchemy_admin/routes"
|
|
3
4
|
|
|
4
5
|
// Matches a URL fragment (#anchor) at the end of a string.
|
|
5
6
|
// Covers RFC 3986 unreserved characters (ALPHA, DIGIT, "-", ".", "_", "~")
|
|
6
7
|
// which are the characters valid in URL fragments and common in DOM element IDs.
|
|
7
8
|
const ANCHOR_REGEX = /#[\w.~-]+$/
|
|
8
9
|
|
|
10
|
+
// The server renders the expression as a JavaScript literal, so that whatever
|
|
11
|
+
// a host application configures keeps the meaning it had as an inline script.
|
|
12
|
+
// A replaced external tab may not render it, the server validates urls anyway.
|
|
13
|
+
function isLinkUrl(url) {
|
|
14
|
+
const literal =
|
|
15
|
+
document.getElementById("external_link")?.dataset.linkUrlRegexp
|
|
16
|
+
if (!literal) return true
|
|
17
|
+
|
|
18
|
+
const end = literal.lastIndexOf("/")
|
|
19
|
+
return new RegExp(literal.slice(1, end), literal.slice(end + 1)).test(url)
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
// Represents the link Dialog that appears, if a user clicks the link buttons
|
|
10
23
|
// in TinyMCE or on an Ingredient that has links enabled (e.g. Picture)
|
|
11
24
|
//
|
|
@@ -13,7 +26,7 @@ export class LinkDialog extends Dialog {
|
|
|
13
26
|
#onCreateLink
|
|
14
27
|
|
|
15
28
|
constructor(link) {
|
|
16
|
-
const url = new URL(
|
|
29
|
+
const url = new URL(routes.link_admin_pages_path, window.location)
|
|
17
30
|
const parameterMapping = {
|
|
18
31
|
url: link.url,
|
|
19
32
|
selected_tab: link.type,
|
|
@@ -109,7 +122,7 @@ export class LinkDialog extends Dialog {
|
|
|
109
122
|
if (linkType === "internal" && elementAnchor.value !== "") {
|
|
110
123
|
// remove possible fragments on the url and attach the fragment (which contains the #)
|
|
111
124
|
url = url.replace(ANCHOR_REGEX, "") + elementAnchor.value
|
|
112
|
-
} else if (linkType === "external" && !url
|
|
125
|
+
} else if (linkType === "external" && !isLinkUrl(url)) {
|
|
113
126
|
// show validation error and prevent link creation
|
|
114
127
|
this.#showValidationError()
|
|
115
128
|
return
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readJSONScript } from "alchemy_admin/utils/json_script"
|
|
2
|
+
|
|
3
|
+
// Member routes are rendered with 1 as the record id and become functions
|
|
4
|
+
// that put the given id in its place.
|
|
5
|
+
function buildRoutes({ collection = {}, member = {} } = {}) {
|
|
6
|
+
const routes = { ...collection }
|
|
7
|
+
|
|
8
|
+
Object.entries(member).forEach(([name, path]) => {
|
|
9
|
+
routes[name] =
|
|
10
|
+
typeof path === "string"
|
|
11
|
+
? (id) => path.replace(/1/, id)
|
|
12
|
+
: buildRoutes({ member: path })
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
return routes
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const routes = buildRoutes(readJSONScript("alchemy_routes"))
|