alchemy_cms 8.3.11 → 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 -27
- 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/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 +52 -11
- 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 +29 -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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Turbo } from "@hotwired/turbo-rails"
|
|
2
|
+
import { closeCurrentDialog } from "alchemy_admin/dialog"
|
|
3
|
+
import { reloadPreview } from "alchemy_admin/components/preview_window"
|
|
4
|
+
import { removeTab } from "alchemy_admin/fixed_elements"
|
|
5
|
+
import IngredientAnchorLink from "alchemy_admin/ingredient_anchor_link"
|
|
6
|
+
import pleaseWaitOverlay from "alchemy_admin/please_wait_overlay"
|
|
7
|
+
|
|
8
|
+
// A dialog form submits into the dialog frame, so a redirect would be followed
|
|
9
|
+
// inside the dialog and render the target page in it. The server answers a
|
|
10
|
+
// successful submission with this action instead: close the dialog, then visit
|
|
11
|
+
// the destination the action would have redirected to.
|
|
12
|
+
Turbo.StreamActions.dialog_visit = function () {
|
|
13
|
+
const url = this.getAttribute("url")
|
|
14
|
+
|
|
15
|
+
if (!closeCurrentDialog(() => Turbo.visit(url))) {
|
|
16
|
+
Turbo.visit(url)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The editors observe their form field for mutations, so it must not be replaced.
|
|
21
|
+
|
|
22
|
+
Turbo.StreamActions.assign_picture = function () {
|
|
23
|
+
const [formField] = this.targetElements
|
|
24
|
+
|
|
25
|
+
if (!formField) return
|
|
26
|
+
|
|
27
|
+
formField.value = this.getAttribute("picture-id")
|
|
28
|
+
formField.setAttribute(
|
|
29
|
+
"data-image-file-width",
|
|
30
|
+
this.getAttribute("image-file-width")
|
|
31
|
+
)
|
|
32
|
+
formField.setAttribute(
|
|
33
|
+
"data-image-file-height",
|
|
34
|
+
this.getAttribute("image-file-height")
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
closeCurrentDialog(() => {
|
|
38
|
+
formField.closest("alchemy-element-editor")?.setDirty()
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Turbo.StreamActions.assign_attachment = function () {
|
|
43
|
+
const [formField] = this.targetElements
|
|
44
|
+
|
|
45
|
+
if (!formField) return
|
|
46
|
+
|
|
47
|
+
const fileEditor = formField.parentElement
|
|
48
|
+
|
|
49
|
+
formField.value = this.getAttribute("attachment-id")
|
|
50
|
+
fileEditor.querySelector(":scope > .file_name").textContent =
|
|
51
|
+
this.getAttribute("attachment-name")
|
|
52
|
+
fileEditor
|
|
53
|
+
.querySelector(":scope > .file_icon")
|
|
54
|
+
.replaceChildren(this.templateContent)
|
|
55
|
+
fileEditor
|
|
56
|
+
.querySelector(":scope > .remove_file_link")
|
|
57
|
+
.classList.remove("hidden")
|
|
58
|
+
|
|
59
|
+
closeCurrentDialog(() => {
|
|
60
|
+
formField.closest("alchemy-element-editor")?.setDirty(formField)
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// An intermediate action that closes the current dialog. This will be gone once
|
|
65
|
+
// all dialogs resolve through a promise and no longer have to be closed
|
|
66
|
+
// implicitly.
|
|
67
|
+
Turbo.StreamActions.close_current_dialog = function () {
|
|
68
|
+
closeCurrentDialog()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
Turbo.StreamActions.reload_preview = function () {
|
|
72
|
+
reloadPreview()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
Turbo.StreamActions.remove_fixed_element = function () {
|
|
76
|
+
removeTab(this.getAttribute("element-id"))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Turbo.StreamActions.update_anchor_icon = function () {
|
|
80
|
+
IngredientAnchorLink.updateIcon(
|
|
81
|
+
this.getAttribute("ingredient-id"),
|
|
82
|
+
this.getAttribute("active") === "true"
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Turbo.StreamActions.hide_please_wait_overlay = function () {
|
|
87
|
+
pleaseWaitOverlay(false)
|
|
88
|
+
}
|
|
@@ -9,7 +9,9 @@ function prepareURL(path, data, method) {
|
|
|
9
9
|
const url = new URL(window.location.origin + path)
|
|
10
10
|
|
|
11
11
|
if (data && isGetRequest(method)) {
|
|
12
|
-
|
|
12
|
+
new URLSearchParams(data).forEach((value, key) => {
|
|
13
|
+
url.searchParams.set(key, value)
|
|
14
|
+
})
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
return url.toString()
|
|
@@ -79,7 +81,16 @@ export default async function ajax(
|
|
|
79
81
|
|
|
80
82
|
if (response.ok) {
|
|
81
83
|
return { data: responseData, status: response.status }
|
|
82
|
-
} else {
|
|
83
|
-
throw responseData || new Error("An error occurred during the transaction")
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
// The session expired while the tab was open. Send the user to the login
|
|
87
|
+
// page instead of leaving them with an error they cannot act on. Never
|
|
88
|
+
// settle, so callers skip their error handling: the login page is what the
|
|
89
|
+
// user needs to see, not a growl about a request they cannot retry.
|
|
90
|
+
if (response.status === 401 && responseData?.redirect_url) {
|
|
91
|
+
Turbo.visit(responseData.redirect_url)
|
|
92
|
+
return new Promise(() => {})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
throw responseData || new Error("An error occurred during the transaction")
|
|
85
96
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Dispatches a bubbling CustomEvent from the given element.
|
|
2
|
+
export function dispatchCustomEvent(element, name, detail = {}) {
|
|
3
|
+
element.dispatchEvent(new CustomEvent(name, { bubbles: true, detail }))
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
export function on(eventName, baseSelector, targetSelector, callback) {
|
|
2
7
|
document.querySelectorAll(baseSelector).forEach((baseNode) => {
|
|
3
8
|
baseNode.addEventListener(eventName, (evt) => {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the data the server rendered into a JSON script tag
|
|
3
|
+
* @param {string} id
|
|
4
|
+
* @returns {any|undefined} undefined if no such script tag is present
|
|
5
|
+
*/
|
|
6
|
+
export function readJSONScript(id) {
|
|
7
|
+
const script = document.getElementById(id)
|
|
8
|
+
|
|
9
|
+
if (!script) return undefined
|
|
10
|
+
|
|
11
|
+
return JSON.parse(script.textContent)
|
|
12
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { translate } from "alchemy_admin/i18n"
|
|
2
|
+
import {
|
|
3
|
+
autoUpdate,
|
|
4
|
+
computePosition,
|
|
5
|
+
flip,
|
|
6
|
+
offset,
|
|
7
|
+
shift,
|
|
8
|
+
size
|
|
9
|
+
} from "@floating-ui/dom"
|
|
10
|
+
|
|
11
|
+
const DROPDOWN_WINDOW_MARGIN = 16
|
|
12
|
+
const DROPDOWN_MIN_HEIGHT = 120
|
|
13
|
+
const DROPDOWN_MAX_WIDTH = 480
|
|
14
|
+
|
|
15
|
+
// Tom Select hides the native element it enhances, so the element can not take
|
|
16
|
+
// focus itself. Focus the Tom Select control instead, falling back to the
|
|
17
|
+
// native focus while Tom Select has not initialized yet. Tom Select based custom
|
|
18
|
+
// elements use this for their focus() override, so acting on an autofocus
|
|
19
|
+
// attribute (e.g. from a dialog) lands on the visible control.
|
|
20
|
+
export function focusTomSelect(tomSelect, focusFallback) {
|
|
21
|
+
if (tomSelect) {
|
|
22
|
+
tomSelect.focus()
|
|
23
|
+
} else {
|
|
24
|
+
focusFallback()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Tom Select's `destroy()` removes the dropdown, but it never closes it, so the
|
|
29
|
+
// `dropdown_close` handler below would not run. Close first, so a select that is
|
|
30
|
+
// torn down while its dropdown is open (e.g. because a Turbo render replaces the
|
|
31
|
+
// form) does not leave the full window dropdown mask behind, swallowing every
|
|
32
|
+
// click.
|
|
33
|
+
export function teardownTomSelect(tomSelect) {
|
|
34
|
+
tomSelect?.close(false)
|
|
35
|
+
tomSelect?.destroy()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Dropdown positioning shared by the Tom Select based components. It appends the
|
|
39
|
+
// dropdown to the body and keeps it positioned with Floating UI so it is not
|
|
40
|
+
// clipped inside dialogs or scrollable panels. The returned handlers are used as
|
|
41
|
+
// Tom Select `onDropdownOpen`/`onDropdownClose` callbacks and are therefore
|
|
42
|
+
// invoked with the Tom Select instance as `this`.
|
|
43
|
+
export function createDropdownPositioning() {
|
|
44
|
+
const dropdownMask = document.createElement("div")
|
|
45
|
+
dropdownMask.className = "ts-dropdown-mask"
|
|
46
|
+
|
|
47
|
+
let removeAutoUpdater = () => {}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
async onDropdownOpen() {
|
|
51
|
+
// Make the dropdown at least as wide as the control.
|
|
52
|
+
const styles = {
|
|
53
|
+
minWidth: `${this.control.offsetWidth}px`
|
|
54
|
+
}
|
|
55
|
+
// If the select is inside a dialog, we need to ensure the dropdown appears above it.
|
|
56
|
+
if (this.control.closest(".alchemy-dialog-body, .alchemy-popover")) {
|
|
57
|
+
styles.zIndex = "101"
|
|
58
|
+
}
|
|
59
|
+
Object.assign(this.dropdown.style, styles)
|
|
60
|
+
// Append the dropdown to the body to avoid overflow issues. Inside a
|
|
61
|
+
// native modal dialog it has to go into the dialog element instead,
|
|
62
|
+
// because content outside of the top layer would be inert and
|
|
63
|
+
// unreachable.
|
|
64
|
+
const dropdownRoot = this.control.closest("dialog") || document.body
|
|
65
|
+
dropdownRoot.append(dropdownMask)
|
|
66
|
+
dropdownRoot.append(this.dropdown)
|
|
67
|
+
// Use Floating UI to position the dropdown relative to the control.
|
|
68
|
+
const updatePosition = async () => {
|
|
69
|
+
// Use Floating UI to calculate the dropdown position
|
|
70
|
+
const { x, y } = await computePosition(this.control, this.dropdown, {
|
|
71
|
+
// Line the dropdown up with the left edge of the control. Without this
|
|
72
|
+
// it is centered on the control and a dropdown grown wide by long
|
|
73
|
+
// entries hangs over into the page left of the field.
|
|
74
|
+
placement: "bottom-start",
|
|
75
|
+
middleware: [
|
|
76
|
+
// Make some space between the control and the dropdown to prevent overlap
|
|
77
|
+
offset(2),
|
|
78
|
+
// Flip to the opposite side if there’s not enough space
|
|
79
|
+
flip(),
|
|
80
|
+
// Keep the dropdown inside the window
|
|
81
|
+
shift({ padding: DROPDOWN_WINDOW_MARGIN }),
|
|
82
|
+
// Ensure the dropdown fits within the viewport
|
|
83
|
+
size({
|
|
84
|
+
padding: DROPDOWN_WINDOW_MARGIN,
|
|
85
|
+
apply({ availableWidth, availableHeight, elements }) {
|
|
86
|
+
// Keep long entries from stretching the dropdown across the
|
|
87
|
+
// window, but never make it narrower than the control.
|
|
88
|
+
Object.assign(elements.floating.style, {
|
|
89
|
+
maxWidth: `max(${elements.reference.offsetWidth}px, min(${availableWidth}px, var(--select-dropdown-max-width, ${DROPDOWN_MAX_WIDTH}px)))`
|
|
90
|
+
})
|
|
91
|
+
Object.assign(
|
|
92
|
+
elements.floating.querySelector(".ts-dropdown-content").style,
|
|
93
|
+
{
|
|
94
|
+
maxHeight: `${Math.max(DROPDOWN_MIN_HEIGHT, availableHeight - DROPDOWN_WINDOW_MARGIN)}px`
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
]
|
|
100
|
+
})
|
|
101
|
+
// Position the dropdown
|
|
102
|
+
Object.assign(this.dropdown.style, {
|
|
103
|
+
left: `${x}px`,
|
|
104
|
+
top: `${y}px`
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
// Update the dropdown position whenever the window resizes or scrolls.
|
|
108
|
+
removeAutoUpdater = autoUpdate(
|
|
109
|
+
this.control,
|
|
110
|
+
this.dropdown,
|
|
111
|
+
updatePosition
|
|
112
|
+
)
|
|
113
|
+
},
|
|
114
|
+
onDropdownClose() {
|
|
115
|
+
this.control_input.classList.remove("has-value")
|
|
116
|
+
this.control_input.blur()
|
|
117
|
+
// Remove the dropdown from DOM when closed.
|
|
118
|
+
this.dropdown.remove()
|
|
119
|
+
dropdownMask.remove()
|
|
120
|
+
// Cleanup the position auto-update when the dropdown is closed.
|
|
121
|
+
removeAutoUpdater()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Configuration for Tom Select's `remove_button` plugin. The plugin renders its
|
|
127
|
+
// control as a non-semantic element (a `<div role="button">` by default); render
|
|
128
|
+
// a real `<button>` instead so assistive tech and the keyboard treat it as one.
|
|
129
|
+
// The translated title also names the icon-only button via `aria-label`, like the
|
|
130
|
+
// clear button does.
|
|
131
|
+
export function removeButton() {
|
|
132
|
+
return {
|
|
133
|
+
title: translate("Remove"),
|
|
134
|
+
html(data) {
|
|
135
|
+
const button = document.createElement("button")
|
|
136
|
+
button.type = "button"
|
|
137
|
+
button.className = data.className
|
|
138
|
+
button.title = data.title
|
|
139
|
+
button.setAttribute("aria-label", data.title)
|
|
140
|
+
button.tabIndex = -1
|
|
141
|
+
button.innerHTML = `<alchemy-icon name="close" size="sm"></alchemy-icon>`
|
|
142
|
+
return button
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Customize the "create" and "no results" dropdown messages with i18n. Shared by
|
|
148
|
+
// the Tom Select based components as `render` functions.
|
|
149
|
+
export const dropdownMessages = {
|
|
150
|
+
option_create(data, escape) {
|
|
151
|
+
return `<div class="create">
|
|
152
|
+
${translate("Add")} <strong>${escape(data.input)}</strong>…
|
|
153
|
+
</div>`
|
|
154
|
+
},
|
|
155
|
+
// Tom Select prepends this above the results it already has, which would
|
|
156
|
+
// shift them.
|
|
157
|
+
loading() {
|
|
158
|
+
if (Object.keys(this.options).length > 0) return
|
|
159
|
+
return `<div class="loading-results">${translate("Please wait, loading")}…</div>`
|
|
160
|
+
},
|
|
161
|
+
no_results() {
|
|
162
|
+
return `<div class="no-results">${translate("No results found")}</div>`
|
|
163
|
+
},
|
|
164
|
+
// Shown at the bottom of the dropdown while the virtual scroll plugin appends
|
|
165
|
+
// the next page of results.
|
|
166
|
+
loading_more: () =>
|
|
167
|
+
`<div class="loading-more">${translate("Loading more results")}…</div>`,
|
|
168
|
+
// Shown at the bottom of the dropdown once every page has been loaded.
|
|
169
|
+
no_more_results: () =>
|
|
170
|
+
`<div class="no-more-results">${translate("No more results")}</div>`
|
|
171
|
+
}
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
// We still use jQuery in some places (ie. select2)
|
|
2
|
-
import "handlebars"
|
|
3
|
-
import "jquery"
|
|
4
1
|
import "@ungap/custom-elements"
|
|
5
2
|
import { Turbo } from "@hotwired/turbo-rails"
|
|
6
|
-
import "select2"
|
|
7
|
-
|
|
8
|
-
import Rails from "@rails/ujs"
|
|
9
3
|
|
|
10
4
|
import { translate } from "alchemy_admin/i18n"
|
|
11
5
|
import { currentDialog, closeCurrentDialog } from "alchemy_admin/dialog"
|
|
12
|
-
import Dirty from "alchemy_admin/dirty"
|
|
13
6
|
import * as FixedElements from "alchemy_admin/fixed_elements"
|
|
14
7
|
import { growl } from "alchemy_admin/growler"
|
|
15
8
|
import Initializer from "alchemy_admin/initializer"
|
|
@@ -19,12 +12,11 @@ import Spinner from "alchemy_admin/spinner"
|
|
|
19
12
|
import { reloadPreview } from "alchemy_admin/components/preview_window"
|
|
20
13
|
import { openConfirmDialog } from "alchemy_admin/confirm_dialog"
|
|
21
14
|
|
|
15
|
+
import "alchemy_admin/turbo_stream_actions"
|
|
16
|
+
|
|
22
17
|
// Web Components
|
|
23
18
|
import "alchemy_admin/components"
|
|
24
19
|
|
|
25
|
-
// Handlebars Templates
|
|
26
|
-
import "alchemy_admin/templates/compiled"
|
|
27
|
-
|
|
28
20
|
// Shoelace Setup
|
|
29
21
|
import "alchemy_admin/shoelace_theme"
|
|
30
22
|
|
|
@@ -37,7 +29,6 @@ if (typeof window.Alchemy === "undefined") {
|
|
|
37
29
|
Object.assign(Alchemy, {
|
|
38
30
|
closeCurrentDialog,
|
|
39
31
|
currentDialog,
|
|
40
|
-
...Dirty,
|
|
41
32
|
t: translate, // Global utility method for translating a given string
|
|
42
33
|
FixedElements,
|
|
43
34
|
growl,
|
|
@@ -46,8 +37,6 @@ Object.assign(Alchemy, {
|
|
|
46
37
|
Spinner,
|
|
47
38
|
reloadPreview
|
|
48
39
|
})
|
|
49
|
-
|
|
50
|
-
Rails.start()
|
|
51
40
|
Turbo.config.forms.confirm = openConfirmDialog
|
|
52
41
|
document.addEventListener("turbo:load", Initializer)
|
|
53
42
|
|
|
@@ -59,9 +48,3 @@ export { ElementSelect } from "alchemy_admin/components/element_select"
|
|
|
59
48
|
export { TagsAutocomplete } from "alchemy_admin/components/tags_autocomplete"
|
|
60
49
|
export { Tinymce } from "alchemy_admin/components/tinymce"
|
|
61
50
|
export { on } from "alchemy_admin/utils/events"
|
|
62
|
-
|
|
63
|
-
// Page-specific modules - bundled to avoid dual-loading
|
|
64
|
-
export { default as ImageCropper } from "alchemy_admin/image_cropper"
|
|
65
|
-
export { default as ImageOverlay } from "alchemy_admin/image_overlay"
|
|
66
|
-
export { default as pictureSelector } from "alchemy_admin/picture_selector"
|
|
67
|
-
export { default as NodeTree } from "alchemy_admin/node_tree"
|
data/app/javascript/preview.js
CHANGED
|
@@ -1,125 +1,143 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
outline: "2px dashed #90b9d0",
|
|
18
|
-
"outline-offset": "4px"
|
|
19
|
-
}
|
|
20
|
-
},
|
|
1
|
+
const STYLES = {
|
|
2
|
+
reset: {
|
|
3
|
+
outline: "",
|
|
4
|
+
"outline-offset": "",
|
|
5
|
+
cursor: ""
|
|
6
|
+
},
|
|
7
|
+
hover: {
|
|
8
|
+
outline: "2px dashed #f0b437",
|
|
9
|
+
"outline-offset": "4px",
|
|
10
|
+
cursor: "pointer"
|
|
11
|
+
},
|
|
12
|
+
selected: {
|
|
13
|
+
outline: "2px dashed #90b9d0",
|
|
14
|
+
"outline-offset": "4px"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
switch (event.data.message) {
|
|
25
|
-
case "Alchemy.blurElements":
|
|
26
|
-
this.blurElements()
|
|
27
|
-
break
|
|
28
|
-
case "Alchemy.focusElement":
|
|
29
|
-
this.focusElement(event.data)
|
|
30
|
-
break
|
|
31
|
-
default:
|
|
32
|
-
console.info("Received unknown message!", event.data)
|
|
33
|
-
}
|
|
34
|
-
})
|
|
35
|
-
this.elements = Array.from(
|
|
36
|
-
document.querySelectorAll("[data-alchemy-element]")
|
|
37
|
-
)
|
|
38
|
-
this.elements.forEach((element) => {
|
|
39
|
-
element.addEventListener("mouseover", () => {
|
|
40
|
-
if (!element.classList.contains("selected")) {
|
|
41
|
-
Object.assign(element.style, this.getStyle("hover"))
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
element.addEventListener("mouseout", () => {
|
|
45
|
-
if (!element.classList.contains("selected")) {
|
|
46
|
-
Object.assign(element.style, this.getStyle("reset"))
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
element.addEventListener("click", (e) => {
|
|
50
|
-
e.stopPropagation()
|
|
51
|
-
e.preventDefault()
|
|
52
|
-
this.selectElement(element)
|
|
53
|
-
this.focusElementEditor(element)
|
|
54
|
-
})
|
|
55
|
-
})
|
|
56
|
-
},
|
|
18
|
+
let elements = []
|
|
19
|
+
let selection = null
|
|
57
20
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
21
|
+
// Mark element in preview frame as selected and scrolls to it.
|
|
22
|
+
function selectElement(element) {
|
|
23
|
+
blurElements(element)
|
|
24
|
+
selection = element
|
|
25
|
+
Object.assign(element.style, STYLES.selected)
|
|
26
|
+
element.scrollIntoView({
|
|
27
|
+
behavior: "smooth",
|
|
28
|
+
block: "start"
|
|
29
|
+
})
|
|
30
|
+
}
|
|
68
31
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
32
|
+
// Blur all elements in preview frame.
|
|
33
|
+
function blurElements(selectedElement) {
|
|
34
|
+
elements.forEach((element) => {
|
|
35
|
+
if (element !== selectedElement) {
|
|
36
|
+
Object.assign(element.style, STYLES.reset)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
selection = null
|
|
40
|
+
}
|
|
78
41
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
} else {
|
|
85
|
-
return console.warn("Could not focus element with id", data.element_id)
|
|
86
|
-
}
|
|
87
|
-
},
|
|
42
|
+
function getElement(elementId) {
|
|
43
|
+
return elements.find(
|
|
44
|
+
(element) => element.dataset.alchemyElement === elementId.toString()
|
|
45
|
+
)
|
|
46
|
+
}
|
|
88
47
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
)
|
|
93
|
-
},
|
|
48
|
+
// Focus the element in the Alchemy preview window.
|
|
49
|
+
function focusElement(data) {
|
|
50
|
+
const element = getElement(data.element_id)
|
|
94
51
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
52
|
+
if (element) {
|
|
53
|
+
selectElement(element)
|
|
54
|
+
} else {
|
|
55
|
+
console.warn("Could not focus element with id", data.element_id)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Focus the element editor in the Alchemy element window.
|
|
60
|
+
function focusElementEditor(element) {
|
|
61
|
+
window.parent.postMessage(
|
|
62
|
+
{
|
|
63
|
+
message: "Alchemy.focusElementEditor",
|
|
64
|
+
element_id: element.dataset.alchemyElement
|
|
105
65
|
},
|
|
66
|
+
window.location.origin
|
|
67
|
+
)
|
|
68
|
+
}
|
|
106
69
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
70
|
+
function observeElement(element) {
|
|
71
|
+
element.addEventListener("mouseover", () => {
|
|
72
|
+
if (element !== selection) {
|
|
73
|
+
Object.assign(element.style, STYLES.hover)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
element.addEventListener("mouseout", () => {
|
|
77
|
+
if (element !== selection) {
|
|
78
|
+
Object.assign(element.style, STYLES.reset)
|
|
113
79
|
}
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function selectClickedElement(event) {
|
|
84
|
+
const element = event.target.closest("[data-alchemy-element]")
|
|
85
|
+
|
|
86
|
+
if (element) {
|
|
87
|
+
selectElement(element)
|
|
88
|
+
focusElementEditor(element)
|
|
114
89
|
}
|
|
115
|
-
})
|
|
116
90
|
|
|
117
|
-
|
|
91
|
+
return element
|
|
92
|
+
}
|
|
118
93
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
94
|
+
function navigates(target) {
|
|
95
|
+
if (target.closest("a")) return true
|
|
96
|
+
|
|
97
|
+
const control = target.closest("button, input")
|
|
98
|
+
return Boolean(control?.form) && ["submit", "image"].includes(control.type)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Client side routers navigate from click handlers on the link itself, so
|
|
102
|
+
// navigating clicks have to be taken before the event reaches the page.
|
|
103
|
+
function onNavigatingClick(event) {
|
|
104
|
+
if (navigates(event.target) && selectClickedElement(event)) {
|
|
105
|
+
event.stopPropagation()
|
|
106
|
+
event.preventDefault()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function init() {
|
|
111
|
+
window.addEventListener("message", (event) => {
|
|
112
|
+
switch (event.data.message) {
|
|
113
|
+
case "Alchemy.blurElements":
|
|
114
|
+
blurElements()
|
|
115
|
+
break
|
|
116
|
+
case "Alchemy.focusElement":
|
|
117
|
+
focusElement(event.data)
|
|
118
|
+
break
|
|
119
|
+
default:
|
|
120
|
+
console.info("Received unknown message!", event.data)
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
window.addEventListener("click", onNavigatingClick, { capture: true })
|
|
125
|
+
window.addEventListener("click", selectClickedElement)
|
|
126
|
+
|
|
127
|
+
elements = Array.from(document.querySelectorAll("[data-alchemy-element]"))
|
|
128
|
+
elements.forEach(observeElement)
|
|
129
|
+
|
|
130
|
+
// Notify parent window that preview is ready
|
|
131
|
+
window.parent.postMessage(
|
|
132
|
+
{
|
|
133
|
+
message: "Alchemy.previewReady"
|
|
134
|
+
},
|
|
135
|
+
window.location.origin
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (document.readyState === "loading") {
|
|
140
|
+
document.addEventListener("DOMContentLoaded", init, { once: true })
|
|
141
|
+
} else {
|
|
142
|
+
init()
|
|
143
|
+
}
|
|
@@ -12,7 +12,7 @@ module Alchemy
|
|
|
12
12
|
all_element_ids = get_all_element_ids(elements, element_ids)
|
|
13
13
|
Element.where(id: all_element_ids).touch_all
|
|
14
14
|
|
|
15
|
-
page_ids = elements.joins(page_version: :page).
|
|
15
|
+
page_ids = elements.joins(page_version: :page).distinct.pluck("alchemy_pages.id")
|
|
16
16
|
Page.where(id: page_ids).touch_all
|
|
17
17
|
end
|
|
18
18
|
|