)]}'
{"version": 3, "sources": ["/html_builder/static/src/builder.js", "/html_builder/static/src/core/anchor/anchor_dialog.js", "/html_builder/static/src/core/anchor/anchor_plugin.js", "/html_builder/static/src/core/builder_action.js", "/html_builder/static/src/core/builder_actions_plugin.js", "/html_builder/static/src/core/builder_component_plugin.js", "/html_builder/static/src/core/builder_content_editable_plugin.js", "/html_builder/static/src/core/builder_options_plugin.js", "/html_builder/static/src/core/builder_options_plugin_translate.js", "/html_builder/static/src/core/builder_overlay/builder_overlay.js", "/html_builder/static/src/core/builder_overlay/builder_overlay_plugin.js", "/html_builder/static/src/core/building_blocks/basic_many2many.js", "/html_builder/static/src/core/building_blocks/builder_button.js", "/html_builder/static/src/core/building_blocks/builder_button_group.js", "/html_builder/static/src/core/building_blocks/builder_checkbox.js", "/html_builder/static/src/core/building_blocks/builder_colorpicker.js", "/html_builder/static/src/core/building_blocks/builder_component.js", "/html_builder/static/src/core/building_blocks/builder_context.js", "/html_builder/static/src/core/building_blocks/builder_datetimepicker.js", "/html_builder/static/src/core/building_blocks/builder_fontfamilypicker.js", "/html_builder/static/src/core/building_blocks/builder_list.js", "/html_builder/static/src/core/building_blocks/builder_many2many.js", "/html_builder/static/src/core/building_blocks/builder_many2one.js", "/html_builder/static/src/core/building_blocks/builder_number_input.js", "/html_builder/static/src/core/building_blocks/builder_range.js", "/html_builder/static/src/core/building_blocks/builder_row.js", "/html_builder/static/src/core/building_blocks/builder_select.js", "/html_builder/static/src/core/building_blocks/builder_select_item.js", "/html_builder/static/src/core/building_blocks/builder_text_input.js", "/html_builder/static/src/core/building_blocks/builder_text_input_base.js", "/html_builder/static/src/core/building_blocks/builder_urlpicker.js", "/html_builder/static/src/core/building_blocks/color_picker_theme_tab.js", "/html_builder/static/src/core/building_blocks/model_many2many.js", "/html_builder/static/src/core/building_blocks/select_many2x.js", "/html_builder/static/src/core/cached_model_plugin.js", "/html_builder/static/src/core/cached_model_utils.js", "/html_builder/static/src/core/clone_plugin.js", "/html_builder/static/src/core/color_style_plugin.js", "/html_builder/static/src/core/color_ui_plugin.js", "/html_builder/static/src/core/compatibility_inline_border_removal_plugin.js", "/html_builder/static/src/core/composite_action_plugin.js", "/html_builder/static/src/core/core_builder_action_plugin.js", "/html_builder/static/src/core/core_plugins.js", "/html_builder/static/src/core/core_setup_editor_plugin.js", "/html_builder/static/src/core/customize_tab_plugin.js", "/html_builder/static/src/core/dependency_manager.js", "/html_builder/static/src/core/disable_snippets_plugin.js", "/html_builder/static/src/core/disable_snippets_plugin_translation.js", "/html_builder/static/src/core/drag_and_drop_move_handle.js", "/html_builder/static/src/core/drag_and_drop_plugin.js", "/html_builder/static/src/core/drop_zone_plugin.js", "/html_builder/static/src/core/dropzone_selector_plugin.js", "/html_builder/static/src/core/field_change_replication_plugin.js", "/html_builder/static/src/core/grid_layout/grid_layout_plugin.js", "/html_builder/static/src/core/icon_plugin.js", "/html_builder/static/src/core/image_plugin.js", "/html_builder/static/src/core/img.js", "/html_builder/static/src/core/img_group.js", "/html_builder/static/src/core/media_website_plugin.js", "/html_builder/static/src/core/move_plugin.js", "/html_builder/static/src/core/operation.js", "/html_builder/static/src/core/operation_plugin.js", "/html_builder/static/src/core/overlay_buttons/overlay_buttons.js", "/html_builder/static/src/core/overlay_buttons/overlay_buttons_plugin.js", "/html_builder/static/src/core/remove_plugin.js", "/html_builder/static/src/core/save_plugin.js", "/html_builder/static/src/core/save_snippet_plugin.js", "/html_builder/static/src/core/select_number_column.js", "/html_builder/static/src/core/setup_editor_plugin.js", "/html_builder/static/src/core/utils.js", "/html_builder/static/src/core/visibility_plugin.js", "/html_builder/static/src/plugins/add_product_option.js", "/html_builder/static/src/plugins/alert_option_plugin.js", "/html_builder/static/src/plugins/background_option/background_hook.js", "/html_builder/static/src/plugins/background_option/background_image_option.js", "/html_builder/static/src/plugins/background_option/background_image_option_plugin.js", "/html_builder/static/src/plugins/background_option/background_option.js", "/html_builder/static/src/plugins/background_option/background_option_plugin.js", "/html_builder/static/src/plugins/background_option/background_position_option.js", "/html_builder/static/src/plugins/background_option/background_position_option_plugin.js", "/html_builder/static/src/plugins/background_option/background_position_overlay.js", "/html_builder/static/src/plugins/background_option/background_shape_option.js", "/html_builder/static/src/plugins/background_option/background_shape_option_plugin.js", "/html_builder/static/src/plugins/background_option/background_shapes_definition.js", "/html_builder/static/src/plugins/badge_option_plugin.js", "/html_builder/static/src/plugins/base_vertical_alignment_option.js", "/html_builder/static/src/plugins/block_alignment_option_plugin.js", "/html_builder/static/src/plugins/border_configurator_option.js", "/html_builder/static/src/plugins/cta_badge_option_plugin.js", "/html_builder/static/src/plugins/date_time_field_plugin.js", "/html_builder/static/src/plugins/font/font_plugin.js", "/html_builder/static/src/plugins/font/font_size_selector.js", "/html_builder/static/src/plugins/image/image_filter_option.js", "/html_builder/static/src/plugins/image/image_filter_option_plugin.js", "/html_builder/static/src/plugins/image/image_format_option.js", "/html_builder/static/src/plugins/image/image_format_option_plugin.js", "/html_builder/static/src/plugins/image/image_helpers.js", "/html_builder/static/src/plugins/image/image_shape_option.js", "/html_builder/static/src/plugins/image/image_shape_option_plugin.js", "/html_builder/static/src/plugins/image/image_shapes_definition.js", "/html_builder/static/src/plugins/image/image_size.js", "/html_builder/static/src/plugins/image/image_size_plugin.js", "/html_builder/static/src/plugins/image/image_snippet_option_plugin.js", "/html_builder/static/src/plugins/image/image_tool_option.js", "/html_builder/static/src/plugins/image/image_tool_option_plugin.js", "/html_builder/static/src/plugins/image/image_transform_option.js", "/html_builder/static/src/plugins/image/image_transform_option_plugin.js", "/html_builder/static/src/plugins/image/replace_media_option.js", "/html_builder/static/src/plugins/image_field_plugin.js", "/html_builder/static/src/plugins/layout_column_option.js", "/html_builder/static/src/plugins/layout_column_option_plugin.js", "/html_builder/static/src/plugins/many2one_option.js", "/html_builder/static/src/plugins/many2one_option_plugin.js", "/html_builder/static/src/plugins/monetary_field_plugin.js", "/html_builder/static/src/plugins/rating_option_plugin.js", "/html_builder/static/src/plugins/separator_option_plugin.js", "/html_builder/static/src/plugins/shadow_option.js", "/html_builder/static/src/plugins/shadow_option_plugin.js", "/html_builder/static/src/plugins/shape/shape_selector.js", "/html_builder/static/src/plugins/text_alignment_option_plugin.js", "/html_builder/static/src/plugins/utils.js", "/html_builder/static/src/plugins/vertical_alignment_option.js", "/html_builder/static/src/plugins/vertical_alignment_option_plugin.js", "/html_builder/static/src/plugins/vertical_justify_option_plugin.js", "/html_builder/static/src/plugins/width_option_plugin.js", "/html_builder/static/src/sidebar/block_tab.js", "/html_builder/static/src/sidebar/custom_inner_snippet.js", "/html_builder/static/src/sidebar/customize_component.js", "/html_builder/static/src/sidebar/customize_tab.js", "/html_builder/static/src/sidebar/invisible_elements_panel.js", "/html_builder/static/src/sidebar/option_container.js", "/html_builder/static/src/sidebar/snippet.js", "/html_builder/static/src/snippets/add_snippet_dialog.js", "/html_builder/static/src/snippets/input_confirmation_dialog.js", "/html_builder/static/src/snippets/snippet_service.js", "/html_builder/static/src/snippets/snippet_viewer.js", "/html_builder/static/src/utils/column_layout_utils.js", "/html_builder/static/src/utils/escaping.js", "/html_builder/static/src/utils/grid_layout_utils.js", "/html_builder/static/src/utils/option_sequence.js", "/html_builder/static/src/utils/scrolling.js", "/html_builder/static/src/utils/sync_cache.js", "/html_builder/static/src/utils/utils.js", "/html_builder/static/src/utils/utils_css.js", "/website/static/src/builder/builder_urlpicker.js", "/website/static/src/builder/option_sequence.js", "/website/static/src/builder/plugins/bootstrap_option_plugin.js", "/website/static/src/builder/plugins/carousel_item_header_buttons.js", "/website/static/src/builder/plugins/carousel_option_plugin.js", "/website/static/src/builder/plugins/carousel_option_translation_plugin.js", "/website/static/src/builder/plugins/collapse_plugin.js", "/website/static/src/builder/plugins/company_team_plugin.js", "/website/static/src/builder/plugins/content_width_option_plugin.js", "/website/static/src/builder/plugins/customize_website_plugin.js", "/website/static/src/builder/plugins/dynamic_svg_option.js", "/website/static/src/builder/plugins/dynamic_svg_option_plugin.js", "/website/static/src/builder/plugins/edit_interaction_plugin.js", "/website/static/src/builder/plugins/floating_blocks/floating_blocks_block_mobile_option.js", "/website/static/src/builder/plugins/floating_blocks/floating_blocks_block_option.js", "/website/static/src/builder/plugins/floating_blocks/floating_blocks_block_option_plugin.js", "/website/static/src/builder/plugins/floating_blocks/floating_blocks_option_plugin.js", "/website/static/src/builder/plugins/font/add_font_dialog.js", "/website/static/src/builder/plugins/font/font_plugin.js", "/website/static/src/builder/plugins/font_awesome_option_plugin.js", "/website/static/src/builder/plugins/form/form_action_fields_option.js", "/website/static/src/builder/plugins/form/form_field_option.js", "/website/static/src/builder/plugins/form/form_field_option_redraw.js", "/website/static/src/builder/plugins/form/form_model_required_field_alert.js", "/website/static/src/builder/plugins/form/form_option.js", "/website/static/src/builder/plugins/form/form_option_add_field_button.js", "/website/static/src/builder/plugins/form/form_option_plugin.js", "/website/static/src/builder/plugins/form/utils.js", "/website/static/src/builder/plugins/highlight/highlight_configurator.js", "/website/static/src/builder/plugins/highlight/highlight_picker.js", "/website/static/src/builder/plugins/highlight/highlight_plugin.js", "/website/static/src/builder/plugins/highlight/stacking_component.js", "/website/static/src/builder/plugins/image/grid_image_option.js", "/website/static/src/builder/plugins/image/grid_image_option_plugin.js", "/website/static/src/builder/plugins/image/image_hover_plugin.js", "/website/static/src/builder/plugins/layout_option/add_element_option.js", "/website/static/src/builder/plugins/layout_option/add_element_option_plugin.js", "/website/static/src/builder/plugins/layout_option/grid_column_option.js", "/website/static/src/builder/plugins/layout_option/grid_column_option_plugin.js", "/website/static/src/builder/plugins/layout_option/layout_option.js", "/website/static/src/builder/plugins/layout_option/layout_option_plugin.js", "/website/static/src/builder/plugins/layout_option/spacing_option.js", "/website/static/src/builder/plugins/layout_option/spacing_option_plugin.js", "/website/static/src/builder/plugins/menu_data_plugin.js", "/website/static/src/builder/plugins/navbar_link_popover/navbar_link_popover.js", "/website/static/src/builder/plugins/options/accordion_option_plugin.js", "/website/static/src/builder/plugins/options/animate_option.js", "/website/static/src/builder/plugins/options/animate_option_plugin.js", "/website/static/src/builder/plugins/options/animate_text.js", "/website/static/src/builder/plugins/options/announcement_scroll_option_plugin.js", "/website/static/src/builder/plugins/options/background_option.js", "/website/static/src/builder/plugins/options/background_option_plugin.js", "/website/static/src/builder/plugins/options/bento_border_option_plugin.js", "/website/static/src/builder/plugins/options/blockquote_option_plugin.js", "/website/static/src/builder/plugins/options/border_option_plugin.js", "/website/static/src/builder/plugins/options/button_option_plugin.js", "/website/static/src/builder/plugins/options/card_image_alignment_option.js", "/website/static/src/builder/plugins/options/card_image_option.js", "/website/static/src/builder/plugins/options/card_image_option_plugin.js", "/website/static/src/builder/plugins/options/card_option.js", "/website/static/src/builder/plugins/options/card_option_plugin.js", "/website/static/src/builder/plugins/options/card_width_option_plugin.js", "/website/static/src/builder/plugins/options/carousel_cards_item_option.js", "/website/static/src/builder/plugins/options/carousel_slides_option_plugin.js", "/website/static/src/builder/plugins/options/chart_option.js", "/website/static/src/builder/plugins/options/chart_option_plugin.js", "/website/static/src/builder/plugins/options/controller_page_listing_layout_option_plugin.js", "/website/static/src/builder/plugins/options/cookies_bar_option.js", "/website/static/src/builder/plugins/options/countdown_option_plugin.js", "/website/static/src/builder/plugins/options/cover_properties_option.js", "/website/static/src/builder/plugins/options/cover_properties_option_plugin.js", "/website/static/src/builder/plugins/options/dynamic_snippet_carousel_option.js", "/website/static/src/builder/plugins/options/dynamic_snippet_carousel_option_plugin.js", "/website/static/src/builder/plugins/options/dynamic_snippet_hook.js", "/website/static/src/builder/plugins/options/dynamic_snippet_option.js", "/website/static/src/builder/plugins/options/dynamic_snippet_option_plugin.js", "/website/static/src/builder/plugins/options/ecomm_categories_showcase_option_plugin.js", "/website/static/src/builder/plugins/options/embed_code_option_dialog.js", "/website/static/src/builder/plugins/options/embed_code_option_plugin.js", "/website/static/src/builder/plugins/options/emphasize_animated_text.js", "/website/static/src/builder/plugins/options/facebook_option_plugin.js", "/website/static/src/builder/plugins/options/faq_horizontal_option_plugin.js", "/website/static/src/builder/plugins/options/footer_copyright_option.js", "/website/static/src/builder/plugins/options/footer_copyright_option_plugin.js", "/website/static/src/builder/plugins/options/footer_option_plugin.js", "/website/static/src/builder/plugins/options/footer_template_option.js", "/website/static/src/builder/plugins/options/gallery_element_option_plugin.js", "/website/static/src/builder/plugins/options/google_maps_option/google_maps_api_key_dialog.js", "/website/static/src/builder/plugins/options/google_maps_option/google_maps_option.js", "/website/static/src/builder/plugins/options/google_maps_option/google_maps_option_plugin.js", "/website/static/src/builder/plugins/options/google_maps_option/google_maps_service.js", "/website/static/src/builder/plugins/options/header/basicHeaderOptionSettings.js", "/website/static/src/builder/plugins/options/header/header_box_option.js", "/website/static/src/builder/plugins/options/header/header_box_option_plugin.js", "/website/static/src/builder/plugins/options/header/header_elements_option.js", "/website/static/src/builder/plugins/options/header/header_font_option.js", "/website/static/src/builder/plugins/options/header/header_icon_background_option.js", "/website/static/src/builder/plugins/options/header/header_navigation_option.js", "/website/static/src/builder/plugins/options/header/header_navigation_option_plugin.js", "/website/static/src/builder/plugins/options/header/header_option_plugin.js", "/website/static/src/builder/plugins/options/header/header_template_option.js", "/website/static/src/builder/plugins/options/header/header_top_options.js", "/website/static/src/builder/plugins/options/image_gallery_option.js", "/website/static/src/builder/plugins/options/image_gallery_option_plugin.js", "/website/static/src/builder/plugins/options/instagram_option_plugin.js", "/website/static/src/builder/plugins/options/language_selector_option.js", "/website/static/src/builder/plugins/options/map_option_plugin.js", "/website/static/src/builder/plugins/options/media_list_item_option.js", "/website/static/src/builder/plugins/options/media_list_option_plugin.js", "/website/static/src/builder/plugins/options/mega_menu_option.js", "/website/static/src/builder/plugins/options/mega_menu_option_plugin.js", "/website/static/src/builder/plugins/options/navbar_logo_option_plugin.js", "/website/static/src/builder/plugins/options/navtabs_header_buttons.js", "/website/static/src/builder/plugins/options/navtabs_header_buttons_plugin.js", "/website/static/src/builder/plugins/options/navtabs_style_option_plugin.js", "/website/static/src/builder/plugins/options/parallax_option.js", "/website/static/src/builder/plugins/options/parallax_option_plugin.js", "/website/static/src/builder/plugins/options/popup_option_plugin.js", "/website/static/src/builder/plugins/options/pricelist_option/pricelist_boxed_option_plugin.js", "/website/static/src/builder/plugins/options/pricelist_option/pricelist_cafe_plugin.js", "/website/static/src/builder/plugins/options/pricelist_option/pricelist_plugin.js", "/website/static/src/builder/plugins/options/pricelist_option/product_catalog_plugin.js", "/website/static/src/builder/plugins/options/process_steps_option.js", "/website/static/src/builder/plugins/options/process_steps_option_plugin.js", "/website/static/src/builder/plugins/options/progress_bar_option_plugin.js", "/website/static/src/builder/plugins/options/scroll_button_option.js", "/website/static/src/builder/plugins/options/scroll_button_option_plugin.js", "/website/static/src/builder/plugins/options/searchbar_option.js", "/website/static/src/builder/plugins/options/searchbar_option_plugin.js", "/website/static/src/builder/plugins/options/social_media_links.js", "/website/static/src/builder/plugins/options/social_media_option_plugin.js", "/website/static/src/builder/plugins/options/table_of_content_option_plugin.js", "/website/static/src/builder/plugins/options/table_of_content_option_plugin_translate.js", "/website/static/src/builder/plugins/options/timeline_list_option_plugin.js", "/website/static/src/builder/plugins/options/timeline_option_plugin.js", "/website/static/src/builder/plugins/options/utils.js", "/website/static/src/builder/plugins/options/visibility_option.js", "/website/static/src/builder/plugins/options/visibility_option_plugin.js", "/website/static/src/builder/plugins/options/website_background_option_plugin.js", "/website/static/src/builder/plugins/options/website_info_option_plugin.js", "/website/static/src/builder/plugins/options/website_page_config_option.js", "/website/static/src/builder/plugins/options/website_page_config_option_plugin.js", "/website/static/src/builder/plugins/popup_visibility_plugin.js", "/website/static/src/builder/plugins/remove_translation_branding_plugin.js", "/website/static/src/builder/plugins/save_translation_plugin.js", "/website/static/src/builder/plugins/setup_editor_plugin.js", "/website/static/src/builder/plugins/size_option_plugin.js", "/website/static/src/builder/plugins/snippets_powerbox_plugin.js", "/website/static/src/builder/plugins/switchable_views.js", "/website/static/src/builder/plugins/switchable_views_plugin.js", "/website/static/src/builder/plugins/theme/theme_advanced_option.js", "/website/static/src/builder/plugins/theme/theme_button_option.js", "/website/static/src/builder/plugins/theme/theme_colors_option.js", "/website/static/src/builder/plugins/theme/theme_fontfamily_option.js", "/website/static/src/builder/plugins/theme/theme_headings_option.js", "/website/static/src/builder/plugins/theme/theme_tab.js", "/website/static/src/builder/plugins/theme/theme_tab_plugin.js", "/website/static/src/builder/plugins/timeline_images_option_plugin.js", "/website/static/src/builder/plugins/translate_announcement_scroll_plugin.js", "/website/static/src/builder/plugins/translate_link_inline_plugin.js", "/website/static/src/builder/plugins/translate_setup_editor_plugin.js", "/website/static/src/builder/plugins/translation_plugin.js", "/website/static/src/builder/plugins/translation_tab/customize_translation_tab.js", "/website/static/src/builder/plugins/translation_tab/customize_translation_tab_plugin.js", "/website/static/src/builder/plugins/translation_tab/translate_webpage_option.js", "/website/static/src/builder/plugins/utils.js", "/website/static/src/builder/plugins/vertical_alignment_option_plugin.js", "/website/static/src/builder/plugins/website_session_plugin.js", "/website/static/src/builder/plugins/website_visibility_plugin.js", "/website/static/src/builder/snippet_model.js", "/website/static/src/builder/snippet_viewer.js", "/website/static/src/builder/translation_components/attributeTranslateDialog.js", "/website/static/src/builder/translation_components/selectTranslateDialog.js", "/website/static/src/builder/translation_components/translatorInfoDialog.js", "/website/static/src/builder/website_builder.js", "/website_payment/static/src/website_builder/donation_option_plugin.js", "/website_payment/static/src/website_builder/supported_payment_methods_option_plugin.js", "/website_sale/static/src/js/website_sale_form_editor.js", "/website_sale/static/src/website_builder/add_to_card_option.js", "/website_sale/static/src/website_builder/add_to_cart_option_plugin.js", "/website_sale/static/src/website_builder/checkout_page_option_plugin.js", "/website_sale/static/src/website_builder/dynamic_snippet_category_item_option_plugin.js", "/website_sale/static/src/website_builder/dynamic_snippet_category_option_plugin.js", "/website_sale/static/src/website_builder/dynamic_snippet_category_options.js", "/website_sale/static/src/website_builder/dynamic_snippet_products_option.js", "/website_sale/static/src/website_builder/dynamic_snippet_products_option_plugin.js", "/website_sale/static/src/website_builder/mega_menu_option.js", "/website_sale/static/src/website_builder/mega_menu_option_plugin.js", "/website_sale/static/src/website_builder/product_attribute_option_plugin.js", "/website_sale/static/src/website_builder/product_header_category_option_plugin.js", "/website_sale/static/src/website_builder/product_header_shop_option_plugin.js", "/website_sale/static/src/website_builder/product_image_option_plugin.js", "/website_sale/static/src/website_builder/product_page_option.js", "/website_sale/static/src/website_builder/product_page_option_plugin.js", "/website_sale/static/src/website_builder/product_ribbon_options.js", "/website_sale/static/src/website_builder/product_ribbon_options_plugin.js", "/website_sale/static/src/website_builder/products_design_panel.js", "/website_sale/static/src/website_builder/products_design_panel_plugin.js", "/website_sale/static/src/website_builder/products_item_option.js", "/website_sale/static/src/website_builder/products_item_option_plugin.js", "/website_sale/static/src/website_builder/products_list_page_option.js", "/website_sale/static/src/website_builder/products_list_page_option_plugin.js", "/website_sale/static/src/website_builder/products_searchbar_option_plugin.js", "/website_sale/static/src/website_builder/shared.js", "/website_sale/static/src/website_builder/website_sale_show_empty_option_plugin.js", "/website_sale/static/src/js/website_sale_utils.js", "/website_generator/static/src/builder/imported_footer_plugin.js", "/website_generator/static/src/builder/imported_footer_template_choice.js", "/website_sale_wishlist/static/src/website_builder/wishlist_page_option_plugin.js", "/website_blog/static/src/website_builder/author_avatar_many2one_plugin.js", "/website_blog/static/src/website_builder/blog_cover_properties_option.js", "/website_blog/static/src/website_builder/blog_page_option_plugin.js", "/website_blog/static/src/website_builder/blog_post_page_option_plugin.js", "/website_blog/static/src/website_builder/blog_post_page_title.js", "/website_blog/static/src/website_builder/blog_post_tags_option.js", "/website_blog/static/src/website_builder/blog_post_tags_option_plugin.js", "/website_blog/static/src/website_builder/blog_searchbar_option_plugin.js", "/website_blog/static/src/website_builder/dynamic_snippet_blog_posts_option.js", "/website_blog/static/src/website_builder/dynamic_snippet_blog_posts_option_plugin.js", "/website_forum/static/src/website_builder/forum_page_option_plugin.js", "/website_forum/static/src/website_builder/forum_searchbar_option_plugin.js"], "mappings": "AAAA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9nBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5pBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5NA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3MA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3LA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACdA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpUA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACndA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/iBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpwBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1LA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpiCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;;;;ACLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtDA;;;;;;;;AAAA;AACA;AACA;AACA;;;;ACHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5QA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;;;;ACLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9dA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClPA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACPA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACdA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7JA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7iBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrrBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzQA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACbA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACbA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzfA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7LA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/dA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7LA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5fA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/YA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACj+BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACbA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/VA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACl6CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACziBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnUA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1RA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACbA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3fA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9LA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACdA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1KA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3LA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACdA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7cA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChOA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3TA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;;;;ACLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzhBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACfA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5QA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;;;;ACJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtQA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACfA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1NA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrdA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3PA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7OA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACTA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACTA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACbA;;;;;;;;AAAA;;;;ACAA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5OA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/TA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7ZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACdA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACbA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA", "sourcesContent": ["import { Editor } from \"@html_editor/editor\";\nimport {\n    Component,\n    EventBus,\n    onMounted,\n    onWillDestroy,\n    onWillStart,\n    onWillUnmount,\n    onWillUpdateProps,\n    status,\n    useRef,\n    useState,\n    useSubEnv,\n} from \"@odoo/owl\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { SIZES, MEDIAS_BREAKPOINTS } from \"@web/core/ui/ui_service\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { addLoadingEffect as addButtonLoadingEffect } from \"@web/core/utils/ui\";\nimport { InvisibleElementsPanel } from \"@html_builder/sidebar/invisible_elements_panel\";\nimport { BlockTab } from \"@html_builder/sidebar/block_tab\";\nimport { CustomizeTab } from \"@html_builder/sidebar/customize_tab\";\nimport { useSnippets } from \"@html_builder/snippets/snippet_service\";\nimport { setBuilderCSSVariables } from \"@html_builder/utils/utils_css\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { getHtmlStyle } from \"@html_editor/utils/formatting\";\nimport { isVisible } from \"@html_builder/utils/utils\";\n\n/**\n * @typedef {(() => void)[]} on_mobile_preview_clicked\n * @typedef {(() => void)[]} trigger_dom_updated\n * @typedef {{ Component: Component; props: object; }[]} lower_panel_entries\n */\n\nexport class Builder extends Component {\n    static template = \"html_builder.Builder\";\n    static components = { BlockTab, CustomizeTab };\n    static props = {\n        closeEditor: { type: Function, optional: true },\n        reloadEditor: { type: Function, optional: true },\n        onEditorLoad: { type: Function, optional: true },\n        installSnippetModule: { type: Function, optional: true },\n        snippetsName: { type: String },\n        toggleMobile: { type: Function },\n        overlayRef: { type: Function },\n        iframeLoaded: { type: Object },\n        isMobile: { type: Boolean },\n        Plugins: { type: Array, optional: true },\n        config: { type: Object, optional: true },\n        getThemeTab: { type: Function, optional: true },\n        editableSelector: { type: String },\n        themeTabDisplayName: { type: String, optional: true },\n        slots: { type: Object, optional: true },\n        getCustomizeTranslationTab: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        config: {},\n        themeTabDisplayName: _t(\"Theme\"),\n    };\n\n    setup() {\n        this.ThemeTab = this.props.getThemeTab?.();\n        this.CustomizeTranslationTab = this.props.getCustomizeTranslationTab?.();\n        // const actionService = useService(\"action\");\n        this.builder_sidebarRef = useRef(\"builder_sidebar\");\n        this.state = useState({\n            canUndo: false,\n            canRedo: false,\n            activeTab: this.props.config.initialTab || \"blocks\",\n            currentOptionsContainers: undefined,\n        });\n        this.invisibleElementsPanelState = useState({\n            invisibleEls: [],\n            invisibleSelector: this.getInvisibleSelector(),\n        });\n        useHotkey(\"control+z\", () => this.undo());\n        useHotkey(\"control+y\", () => this.redo());\n        useHotkey(\"control+shift+z\", () => this.redo());\n        this.orm = useService(\"orm\");\n        this.ui = useService(\"ui\");\n        this.notification = useService(\"notification\");\n\n        this.snippetModel = useSnippets(this.props.snippetsName);\n\n        this.lastTrigerUpdateId = 0;\n        this.editorBus = new EventBus();\n        this.colorPresetToShow = null;\n        this.activeTargetEl = null;\n        const mobileBreakpoint = this.props.config.mobileBreakpoint ?? \"lg\";\n\n        // TODO: maybe do a different config for the translate mode and the\n        // \"regular\" mode.\n        /** @type {Editor} */\n        this.editor = new Editor(\n            {\n                Plugins: this.props.Plugins,\n                ...this.props.config,\n                mobileBreakpoint,\n                isMobileView: (targetEl) => {\n                    const mobileViewThreshold =\n                        MEDIAS_BREAKPOINTS[SIZES[mobileBreakpoint.toUpperCase()]].minWidth;\n                    const clientWidth =\n                        targetEl.ownerDocument.defaultView?.frameElement?.clientWidth ||\n                        targetEl.ownerDocument.documentElement.clientWidth;\n                    return !!clientWidth && clientWidth < mobileViewThreshold;\n                },\n                onChange: ({ isPreviewing }) => {\n                    if (!isPreviewing) {\n                        this.state.canUndo = this.editor.shared.history.canUndo();\n                        this.state.canRedo = this.editor.shared.history.canRedo();\n                        this.updateInvisibleEls();\n                        this.editorBus.trigger(\"UPDATE_EDITING_ELEMENT\");\n                        this.triggerDomUpdated();\n                        this.props.config.onChange?.();\n                    }\n                },\n                reloadEditor: async (param = {}) => {\n                    await this.props.reloadEditor?.({\n                        initialTab: this.state.activeTab,\n                        ...param,\n                    });\n                },\n                closeEditor: async () => {\n                    await this.props.closeEditor?.();\n                },\n                installSnippetModule: (snippet) => this.props.installSnippetModule?.(snippet),\n                /** @type {import(\"plugins\").BuilderResources} */\n                resources: {\n                    trigger_dom_updated: () => {\n                        this.triggerDomUpdated();\n                    },\n                    on_mobile_preview_clicked: withSequence(20, () => {\n                        this.triggerDomUpdated();\n                    }),\n                    before_save_handlers: () => {\n                        const snippetMenuEl = this.builder_sidebarRef.el;\n                        const saveButton = snippetMenuEl.querySelector(\"[data-action='save']\");\n                        delete this.removeLoadingEffect;\n                        if (saveButton) {\n                            // Add a loading effect on the save button and disable the other actions\n                            this.removeLoadingEffect = addButtonLoadingEffect(\n                                snippetMenuEl.querySelector(\"[data-action='save']\")\n                            );\n                        }\n                        this.actionButtonEls = snippetMenuEl.querySelectorAll(\"[data-action]\");\n                        for (const actionButtonEl of this.actionButtonEls) {\n                            actionButtonEl.disabled = true;\n                        }\n                    },\n                    after_save_handlers: () => {\n                        for (const actionButtonEl of this.actionButtonEls) {\n                            actionButtonEl.removeAttribute(\"disabled\");\n                        }\n                        this.removeLoadingEffect?.();\n                    },\n                    on_snippet_dropped_handlers: () => {\n                        this.activeTargetEl = null;\n                    },\n                    change_current_options_containers_listeners: (currentOptionsContainers) => {\n                        this.state.currentOptionsContainers = currentOptionsContainers;\n                        if (!currentOptionsContainers.length) {\n                            // If there is no option, fallback on the current\n                            // fallback tab.\n                            this.setTab(this.noSelectionTab);\n                            return;\n                        }\n                        this.activeTargetEl = null;\n                        this.setTab(\"customize\");\n                    },\n                    lower_panel_entries: withSequence(20, {\n                        Component: InvisibleElementsPanel,\n                        props: this.invisibleElementsPanelState,\n                    }),\n                    unsplittable_node_predicates: (/** @type {Node} */ node) =>\n                        node.querySelector?.(\"[data-oe-translation-source-sha]\"),\n                },\n                localOverlayContainers: {\n                    key: this.env.localOverlayContainerKey,\n                    ref: this.props.overlayRef,\n                },\n                saveSnippet: (snippetEl, cleanForSaveHandlers, wrapWithSaveSnippetHandlers) =>\n                    this.snippetModel.saveSnippet(\n                        snippetEl,\n                        cleanForSaveHandlers,\n                        wrapWithSaveSnippetHandlers\n                    ),\n                snippetModel: this.snippetModel,\n                updateInvisibleElementsPanel: () => this.updateInvisibleEls(),\n                allowCustomStyle: true,\n                allowTargetBlank: true,\n                dropImageAsAttachment: true,\n                getAnimateTextConfig: () => ({ editor: this.editor, editorBus: this.editorBus }),\n                baseContainers: [\"P\"],\n                cleanEmptyStructuralContainers: false,\n                isEditableRTL: false,\n            },\n            this.env.services\n        );\n        this.props.onEditorLoad?.(this.editor);\n\n        onWillStart(async () => {\n            await this.snippetModel.load();\n            // Ensure that the iframe is loaded and the editor is created before\n            // instantiating the sub components that potentially need the\n            // editor.\n            const iframeEl = await this.props.iframeLoaded;\n            if (status(this) === \"destroyed\") {\n                return;\n            }\n            this.editableEl = iframeEl.contentDocument.body.querySelector(\n                this.props.editableSelector\n            );\n\n            if (this.editableEl.matches(\".o_rtl\")) {\n                this.editor.config.isEditableRTL = true;\n            }\n\n            // Prevent image dragging in the website builder. Not via css because\n            // if one of the image ancestor has a dragstart listener, the dragstart handler\n            // can be called with the image as target.\n            this.onDragStart = (ev) => {\n                if (ev.target.nodeName === \"IMG\") {\n                    ev.preventDefault();\n                    ev.stopPropagation();\n                }\n            };\n            this.editor.attachTo(this.editableEl);\n        });\n\n        useSubEnv({\n            editor: this.editor,\n            editorBus: this.editorBus,\n            triggerDomUpdated: this.triggerDomUpdated.bind(this),\n            editColorCombination: this.editColorCombination.bind(this),\n        });\n        onWillDestroy(() => {\n            this.editor.destroy();\n        });\n\n        onMounted(() => {\n            this.editor.document.body.classList.add(\"editor_enable\");\n            setBuilderCSSVariables(getHtmlStyle(this.editor.document));\n            // TODO: onload editor\n            this.updateInvisibleEls();\n            this.editableEl.addEventListener(\"dragstart\", this.onDragStart);\n        });\n        onWillUnmount(() => {\n            this.editableEl.removeEventListener(\"dragstart\", this.onDragStart);\n        });\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.isMobile !== this.props.isMobile) {\n                this.updateInvisibleEls(nextProps.isMobile);\n                this.invisibleElementsPanelState.invisibleSelector = this.getInvisibleSelector(\n                    nextProps.isMobile\n                );\n            }\n        });\n        // Fallback tab when no option is active.\n        this.noSelectionTab = \"blocks\";\n    }\n    async triggerDomUpdated() {\n        this.lastTrigerUpdateId++;\n        const currentTriggerId = this.lastTrigerUpdateId;\n        const getStatePromises = [];\n        const { promise: updatePromise, resolve } = Promise.withResolvers();\n        this.editorBus.trigger(\"DOM_UPDATED\", { getStatePromises, updatePromise });\n        await Promise.all(getStatePromises);\n        const isLastTriggerId = this.lastTrigerUpdateId === currentTriggerId;\n        resolve(isLastTriggerId);\n    }\n\n    get displayOnlyCustomizeTab() {\n        return this.props.config.isTranslationMode;\n    }\n\n    getInvisibleSelector(isMobile = this.props.isMobile) {\n        return `.o_snippet_invisible, ${\n            isMobile ? \".o_snippet_mobile_invisible\" : \".o_snippet_desktop_invisible\"\n        }`;\n    }\n\n    /**\n     * Called when clicking on a tab. Sets the active tab to the given tab.\n     *\n     * @param {String} tab the tab to set\n     * @param {Number | null} presetId the color preset expanding on \"theme\" tab\n     * open.\n     */\n    onTabClick(tab, presetId = null) {\n        if (this.state.activeTab === tab) {\n            // If the tab is already active, do nothing.\n            return;\n        }\n        this.setTab(tab);\n        // Deactivate the options when clicking on the \"BLOCKS\" or \"THEME\" tabs.\n        if (tab === \"theme\" || tab === \"blocks\") {\n            this.colorPresetToShow = presetId;\n            this.activeTargetEl = this.activeTargetEl || this.getActiveTarget();\n            this.editor.shared.builderOptions.deactivateContainers();\n        } else if (this.activeTargetEl) {\n            if (isVisible(this.activeTargetEl)) {\n                // Reactivate the previously active element.\n                this.editor.shared.builderOptions.updateContainers(this.activeTargetEl);\n            }\n            this.activeTargetEl = null;\n        }\n    }\n\n    setTab(tab) {\n        this.state.activeTab = tab;\n        // Set the fallback tab on the \"THEME\" tab if it was selected.\n        this.noSelectionTab = tab === \"theme\" ? \"theme\" : \"blocks\";\n    }\n\n    undo() {\n        this.editor.shared.history.undo();\n    }\n\n    redo() {\n        this.editor.shared.history.redo();\n    }\n\n    onMobilePreviewClick() {\n        this.props.toggleMobile();\n        this.editor.resources[\"on_mobile_preview_clicked\"].forEach((handler) => handler());\n    }\n\n    updateInvisibleEls(isMobile = this.props.isMobile) {\n        this.invisibleElementsPanelState.invisibleEls = [\n            ...this.editor.editable.querySelectorAll(this.getInvisibleSelector(isMobile)),\n        ];\n    }\n\n    lowerPanelEntries() {\n        return this.editor.resources[\"lower_panel_entries\"] ?? [];\n    }\n\n    editColorCombination(presetId) {\n        this.onTabClick(\"theme\", presetId);\n    }\n\n    getActiveTarget() {\n        return this.editor.shared[\"builderOptions\"].getContainers().at(-1)?.element;\n    }\n}\n", "import { Component, useRef, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nexport class AnchorDialog extends Component {\n    static template = \"html_builder.AnchorDialog\";\n    static components = { Dialog };\n    static props = {\n        currentAnchorName: { type: String },\n        renameAnchor: { type: Function },\n        deleteAnchor: { type: Function },\n        formatAnchor: { type: Function },\n        close: { type: Function },\n    };\n\n    setup() {\n        this.title = _t(\"Link Anchor\");\n        this.inputRef = useRef(\"anchor-input\");\n        this.state = useState({ isValid: true });\n    }\n\n    async onConfirmClick() {\n        const newAnchorName = this.props.formatAnchor(this.inputRef.el.value);\n        if (newAnchorName === this.props.currentAnchorName) {\n            this.props.close();\n        }\n\n        this.state.isValid = await this.props.renameAnchor(newAnchorName);\n        if (this.state.isValid) {\n            this.props.close();\n        }\n    }\n\n    onRemoveClick() {\n        this.props.deleteAnchor();\n        this.props.close();\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { markup } from \"@odoo/owl\";\nimport { AnchorDialog } from \"./anchor_dialog\";\nimport { getElementsWithOption } from \"@html_builder/utils/utils\";\n\nconst anchorSelector = \":not(p).oe_structure > *, :not(p)[data-oe-type=html] > *\";\nconst anchorExclude =\n    \".modal *, .oe_structure .oe_structure *, [data-oe-type=html] .oe_structure *, .s_popup\";\n\nexport function canHaveAnchor(element) {\n    return element.matches(anchorSelector) && !element.matches(anchorExclude);\n}\n\n/**\n * @typedef { Object } AnchorShared\n * @property { AnchorPlugin['createOrEditAnchorLink'] } createOrEditAnchorLink\n */\nexport class AnchorPlugin extends Plugin {\n    static id = \"anchor\";\n    static dependencies = [\"history\"];\n    static shared = [\"createOrEditAnchorLink\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        on_cloned_handlers: this.onCloned.bind(this),\n        get_options_container_top_buttons: withSequence(\n            0,\n            this.getOptionsContainerTopButtons.bind(this)\n        ),\n    };\n\n    onCloned({ cloneEl }) {\n        const anchorEls = getElementsWithOption(cloneEl, anchorSelector, anchorExclude);\n        anchorEls.forEach((anchorEl) => this.deleteAnchor(anchorEl));\n    }\n\n    getOptionsContainerTopButtons(el) {\n        if (!canHaveAnchor(el)) {\n            return [];\n        }\n\n        return [\n            {\n                class: \"fa fa-fw fa-link oe_snippet_anchor btn o-hb-btn btn-accent-color-hover\",\n                title: _t(\"Create and copy a link targeting this block or edit it\"),\n                handler: this.createOrEditAnchorLink.bind(this),\n            },\n        ];\n    }\n\n    // TODO check if no other way when doing popup options.\n    isModal(element) {\n        return element.classList.contains(\"modal\");\n    }\n\n    setAnchorName(element, value) {\n        if (value) {\n            element.id = value;\n            if (!this.isModal(element)) {\n                element.dataset.anchor = true;\n            }\n        } else {\n            this.deleteAnchor(element);\n        }\n        this.dependencies.history.addStep();\n    }\n\n    createAnchor(element) {\n        const titleEls = element.querySelectorAll(\"h1, h2, h3, h4, h5, h6\");\n        const title = titleEls.length > 0 ? titleEls[0].innerText : element.dataset.name;\n        const anchorName = this.formatAnchor(title);\n\n        let n = \"\";\n        while (this.document.getElementById(anchorName + n)) {\n            n = (n || 1) + 1;\n        }\n\n        this.setAnchorName(element, anchorName + n);\n    }\n\n    deleteAnchor(element) {\n        element.removeAttribute(\"data-anchor\");\n        element.removeAttribute(\"id\");\n    }\n\n    getAnchorLink(element) {\n        const pathName = this.isModal(element) ? \"\" : this.document.location.pathname;\n        return `${pathName}#${element.id}`;\n    }\n\n    async createOrEditAnchorLink(element) {\n        if (!element.id) {\n            this.createAnchor(element);\n        }\n        const anchorLink = this.getAnchorLink(element);\n        await browser.navigator.clipboard.writeText(anchorLink);\n        const message = _t(\n            \"Anchor copied to clipboard%(br)s%(open_span)sLink: %(anchor_link)s%(close_span)s\",\n            {\n                open_span: markup`<span style=\" display: -webkit-box; -webkit-line-clamp: 1;\n                    -webkit-box-orient: vertical; overflow: hidden;\">`,\n                anchor_link: anchorLink,\n                br: markup`<br>`,\n                close_span: markup`</span>`,\n            }\n        );\n        const closeNotification = this.services.notification.add(message, {\n            type: \"success\",\n            buttons: [\n                {\n                    name: _t(\"Edit\"),\n                    primary: true,\n                    onClick: () => {\n                        closeNotification();\n                        // Open the \"rename anchor\" dialog.\n                        this.services.dialog.add(AnchorDialog, {\n                            currentAnchorName: decodeURIComponent(element.id),\n                            renameAnchor: async (anchorName) => {\n                                const alreadyExists = !!this.document.getElementById(anchorName);\n                                if (alreadyExists) {\n                                    return false;\n                                }\n\n                                this.setAnchorName(element, anchorName);\n                                await this.createOrEditAnchorLink(element);\n                                return true;\n                            },\n                            deleteAnchor: () => {\n                                this.deleteAnchor(element);\n                                this.dependencies.history.addStep();\n                            },\n                            formatAnchor: this.formatAnchor,\n                        });\n                    },\n                },\n            ],\n        });\n    }\n\n    formatAnchor(text) {\n        return encodeURIComponent(text.trim().replace(/\\s+/g, \"-\"));\n    }\n}\n", "/**\n * @typedef { import(\"../../../../html_editor/static/src/editor\").EditorContext } EditorContext\n * @typedef { any } LoadResult\n * @typedef { any } ActionValue\n * @typedef {{\n *  mainParam: any,\n *  [param: string]: any,\n * }} ActionParams\n * In the XML template, the `actionParam` prop can take 2 kinds of values: an\n * Object or something else (string, array, function...).\n * If `actionParams` takes something that is not a object, it is passed as\n * `mainParam`.\n * If `actionParams` takes an object, each key is forwarded to the parameter.\n * e.g.:\n *     <BuilderButton action=\"'customAction'\" actionParam=\"{ customKey: 0 }\"/>\n * is passed as\n *     `params: { customKey: 0 }`\n *\n * @typedef { Object } NextBuilderAction\n * @property { ActionValue } actionValue\n * @property { ActionParams } actionParam\n *\n * SelectableContext is available on BuilderComponents that make the user choose\n * among a list of items (i.e. BuilderSelect and BuilderButtonGroup).\n * @typedef { Object } SelectableContext\n * @property { Object[] } items\n */\n\n/**\n * @typedef { import(\"../../../../html_editor/static/src/editor\").EditorContext } EditorContext\n */\n\nexport class BuilderAction {\n    /** @type { string[] } */\n    static dependencies = [];\n    /**\n     * @param { EditorContext } context\n     */\n    constructor(context) {\n        /** @type { EditorContext['document'] } **/\n        this.document = context.document;\n        this.window = context.document.defaultView;\n        /** @type { EditorContext['editable'] } **/\n        this.editable = context.editable;\n        /** @type { EditorContext['config'] } **/\n        this.config = context.config;\n        /** @type { EditorContext['services'] } **/\n        this.services = context.services;\n        /** @type { EditorContext['dependencies'] } **/\n        this.dependencies = context.dependencies;\n        /** @type { EditorContext['getResource'] } **/\n        this.getResource = context.getResource;\n        /** @type { EditorContext['dispatchTo'] } **/\n        this.dispatchTo = context.dispatchTo;\n        /** @type { EditorContext['delegateTo'] } **/\n        this.delegateTo = context.delegateTo;\n\n        this.setup();\n\n        // Preview is enabled by default in non-reload actions,\n        // and disabled by default in reload actions.\n        this.preview ??= this.reload ? false : true;\n        this.withLoadingEffect ??= true;\n        this.loadOnClean ??= false;\n    }\n\n    /**\n     * Called after dependencies and services are assigned.\n     * Subclasses override this instead of the constructor.\n     */\n    setup() {\n        // Optional override in subclasses\n    }\n\n    /**\n     * Prepare some asynchronous call `onWillStart` and `onWillUpdateProps` of\n     * the BuilderComponent.\n     * A good practice is to use a caching strategy for the data fetched here.\n     *\n     * @param { Object } context\n     * @param { ActionParams } context.actionParam\n     * @param { ActionValue } context.actionValue\n     */\n    async prepare(context) {}\n\n    /**\n     * Set a priority to the action in comparison with its sibling components.\n     * Used in combination with selectable builder components (i.e.\n     * `BuilderButtonGroup` or `BuilderSelect`) to determine which button/select\n     * item should be active when several items could qualify. The item with the\n     * highest priority is ultimately validated as active.\n     *\n     * e.g.: `classAction`'s priority is determined by the number of classes\n     * that apply. If button A sets no class (priority = 0), button B sets\n     * \".some-class\" (priority = 1) and button C sets\n     * \".some-class.some-other-class\" (priority = 2), the active button is the\n     * one that has the most classes applied to the editingElement.\n     *\n     * @param { Object } context\n     * @param { ActionParams } context.params\n     * @param { ActionValue } context.value\n     * @returns { number } default=0\n     */\n    getPriority(context) {}\n\n    /**\n     * Apply the action on the editing element. Can be async.\n     * `apply` is called on preview (if preview=true), on apply, and on clean if\n     * `clean` is not defined on the action.\n     * In cases with an async `apply` and `preview=true`, @see load.\n     *\n     * @param { Object } context\n     * @param { import(\"./dependency_manager\").DependencyManager } context.dependencyManager\n     * @param { boolean } context.isPreviewing\n     * @param { HTMLElement } context.editingElement\n     * @param { ActionValue } context.value\n     * @param { ActionParams } [context.params]\n     * @param { LoadResult } [context.loadResult]\n     * @param { SelectableContext } [context.selectableContext]\n     */\n    async apply(context) {}\n\n    /**\n     * Return the current value of the action on the element.\n     * Used in combination with inputs (BuilderTextInput, BuilderNumberInput,\n     * BuilderCheckbox, BuilderRange, BuilderDateTimePicker...). For other\n     * components, @see isApplied.\n     *\n     * @param { Object } context\n     * @param { HTMLElement } context.editingElement\n     * @param { ActionParams } [context.params]\n     * @returns { any }\n     */\n    getValue(context) {}\n\n    /**\n     * Whether the action is already applied.\n     * Used in combination with builder components that can only be active or\n     * not (i.e. not inputs). For inputs, @see getValue.\n     *\n     * @param { Object } context\n     * @param { HTMLElement } context.editingElement\n     * @param { ActionValue } context.value\n     * @param { ActionParams } [context.params]\n     * @returns {boolean}\n     */\n    isApplied(context) {}\n\n    /**\n     * Clean/reset the value if needed. Can be async.\n     * If not defined, `apply` will be called instead.\n     *\n     * @param { Object } context\n     * @param { import(\"./dependency_manager\").DependencyManager } context.dependencyManager\n     * @param { NextBuilderAction } context.nextAction\n     * @param { boolean } context.isPreviewing\n     * @param { HTMLElement } context.editingElement\n     * @param { ActionValue } context.value\n     * @param { ActionParams } [context.params]\n     * @param { LoadResult } [context.loadResult]\n     * @param { SelectableContext } [context.selectableContext]\n     */\n    async clean(context) {}\n\n    /**\n     * Load and return some data before calling `apply` if needed. The return\n     * value is passed as `loadResult` in the `apply` context.\n     * /!\\ By itself, `load` SHOULD NOT have any effect.\n     *\n     * Should be used when there is a preview: when triggering an action after\n     * another one, the previous call to `apply` is cancelled. But if `apply` is\n     * async, the builder has to wait for the end of the call (and clean) before\n     * applying the next action. In order to avoid stalling the builder, you can\n     * use `load`: the asynchronous code will always run before `apply`, but if\n     * the user then triggers another action, we do not wait for `load` to\n     * finish and we process the new preview directly.\n     *\n     * @param { Object } context\n     * @param { HTMLElement } context.editingElement\n     * @returns { Promise<LoadResult> }\n     */\n    async load(context) {}\n\n    /**\n     * Check if a BuilderAction method has been overridden and should therefore\n     * be taken into account.\n     *\n     * @param { \"prepare\"|\"getPriority\"|\"load\"|\"apply\"|\"clean\"|\"isApplied\"|\"getValue\" } method\n     * @returns { boolean }\n     */\n    has(method) {\n        const baseMethod = BuilderAction.prototype[method];\n        const actualMethod = this.constructor.prototype[method];\n        return baseMethod !== actualMethod;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\n\n/**\n * @typedef { import(\"./builder_action\").BuilderAction } BuilderAction\n * @typedef {} BuilderActionConstructor\n *\n * @typedef { Object } BuilderActionsShared\n * @property { BuilderActionsPlugin['getAction'] } getAction\n * @property { BuilderActionsPlugin['applyAction'] } applyAction\n */\n\n/** @typedef {BuilderAction[]} builder_actions */\n\nexport class BuilderActionsPlugin extends Plugin {\n    static id = \"builderActions\";\n    static shared = [\"getAction\", \"applyAction\"];\n    static dependencies = [\"operation\", \"history\"];\n    setup() {\n        /** @type { BuilderAction[] } */\n        this.actions = {};\n        for (const actions of this.getResource(\"builder_actions\")) {\n            for (const Action of Object.values(actions)) {\n                if (Action.id in this.actions) {\n                    throw new Error(`Duplicate builder action id: ${Action.id}`);\n                }\n                /** @type { BuilderAction } */\n                this.actions[Action.id] = new Action(\n                    this.__editor.getEditorContext(Action.dependencies)\n                );\n            }\n        }\n        Object.freeze(this.actions);\n    }\n\n    /**\n     * Get the action object for the given action ID.\n     *\n     * @param {string} actionId\n     * @returns {Object}\n     */\n    getAction(actionId) {\n        const action = this.actions[actionId];\n        if (!action) {\n            throw new Error(`Unknown builder action id: ${actionId}`);\n        }\n        return action;\n    }\n\n    /**\n     * Apply action for the given action ID.\n     *\n     * @param {string} actionId\n     * @param {Object} spec\n     */\n    applyAction(actionId, spec) {\n        const action = this.getAction(actionId);\n        this.dependencies.operation.next(\n            async () => {\n                await action.apply(spec);\n                this.dependencies.history.addStep();\n            },\n            {\n                ...action,\n                load: async () => {\n                    if (action.load) {\n                        const loadResult = await action.load(spec);\n                        spec.loadResult = loadResult;\n                    }\n                },\n            }\n        );\n    }\n}\n", "import { BuilderList } from \"@html_builder/core/building_blocks/builder_list\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { BuilderButtonGroup } from \"./building_blocks/builder_button_group\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { BuilderDateTimePicker } from \"./building_blocks/builder_datetimepicker\";\nimport { BuilderRow } from \"./building_blocks/builder_row\";\nimport { BuilderButton } from \"./building_blocks/builder_button\";\nimport { BuilderNumberInput } from \"./building_blocks/builder_number_input\";\nimport { BuilderSelect } from \"./building_blocks/builder_select\";\nimport { BuilderSelectItem } from \"./building_blocks/builder_select_item\";\nimport { BuilderColorPicker } from \"./building_blocks/builder_colorpicker\";\nimport { BuilderTextInput } from \"./building_blocks/builder_text_input\";\nimport { BuilderCheckbox } from \"./building_blocks/builder_checkbox\";\nimport { BuilderRange } from \"./building_blocks/builder_range\";\nimport { BuilderContext } from \"./building_blocks/builder_context\";\nimport { BasicMany2Many } from \"./building_blocks/basic_many2many\";\nimport { BuilderMany2Many } from \"./building_blocks/builder_many2many\";\nimport { BuilderMany2One } from \"./building_blocks/builder_many2one\";\nimport { ModelMany2Many } from \"./building_blocks/model_many2many\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { Img } from \"./img\";\nimport { BuilderUrlPicker } from \"./building_blocks/builder_urlpicker\";\nimport { BuilderFontFamilyPicker } from \"./building_blocks/builder_fontfamilypicker\";\n\n/** @typedef {import(\"@odoo/owl\").Component} Component */\n/**\n * @typedef { Object } BuilderComponentShared\n * @property { BuilderComponentPlugin['getComponents'] } getComponents\n */\n\n/** @typedef {Component[]} builder_components */\n\nexport class BuilderComponentPlugin extends Plugin {\n    static id = \"builderComponents\";\n    static shared = [\"getComponents\"];\n\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_components: {\n            BuilderContext,\n            BuilderFontFamilyPicker,\n            BuilderRow,\n            BuilderUrlPicker,\n            Dropdown,\n            DropdownItem,\n            BuilderButtonGroup,\n            BuilderButton,\n            BuilderTextInput,\n            BuilderNumberInput,\n            BuilderRange,\n            BuilderColorPicker,\n            BuilderSelect,\n            BuilderSelectItem,\n            BuilderCheckbox,\n            BasicMany2Many,\n            BuilderMany2Many,\n            BuilderMany2One,\n            ModelMany2Many,\n            BuilderDateTimePicker,\n            BuilderList,\n            Img,\n        },\n    };\n\n    setup() {\n        this.Components = {};\n        for (const r of this.getResource(\"builder_components\")) {\n            for (const C in r) {\n                if (C in this.Components) {\n                    throw new Error(`Duplicated builder component: ${C}`);\n                }\n                this.Components[C] = r[C];\n            }\n        }\n    }\n\n    getComponents() {\n        return this.Components;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { registry } from \"@web/core/registry\";\n\n/** @typedef {import(\"plugins\").CSSSelector} CSSSelector */\n/**\n * @typedef {CSSSelector[]} content_editable_selectors\n * @typedef {CSSSelector[]} content_not_editable_selectors\n */\n\nexport class BuilderContentEditablePlugin extends Plugin {\n    static id = \"builderContentEditablePlugin\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        content_not_editable_selectors: [\n            \"section:has(> .o_container_small, > .container, > .container-fluid)\",\n            \".o_not_editable\",\n            \"[data-oe-field='arch']:empty\",\n        ],\n        content_editable_selectors: [\n            \"section > .o_container_small\",\n            \"section > .container\",\n            \"section > .container-fluid\",\n            \".o_editable\",\n        ],\n        valid_contenteditable_predicates: this.isValidContentEditable.bind(this),\n        content_editable_providers: this.getContentEditableEls.bind(this),\n        content_not_editable_providers: this.getContentNotEditableEls.bind(this),\n        contenteditable_to_remove_selector: \"[contenteditable]\",\n    };\n\n    setup() {\n        this.editable.setAttribute(\"contenteditable\", false);\n    }\n\n    getContentEditableEls(rootEl) {\n        const editableSelector = this.getResource(\"content_editable_selectors\").join(\",\");\n        return [...selectElements(rootEl, editableSelector)];\n    }\n\n    getContentNotEditableEls(rootEl) {\n        const notEditableSelector = this.getResource(\"content_not_editable_selectors\").join(\",\");\n        return [...selectElements(rootEl, notEditableSelector)];\n    }\n\n    isValidContentEditable(contentEditableEl) {\n        // Check if an element is inside a \".o_not_editable\" element that is not\n        // inside a snippet.\n        const isDescendantOfNotEditableNotSnippet = (el) => {\n            let notEditableEl = el.closest(\".o_not_editable\");\n            if (!notEditableEl) {\n                return false;\n            }\n            while (notEditableEl.parentElement.closest(\".o_not_editable\")) {\n                notEditableEl = notEditableEl.parentElement.closest(\".o_not_editable\");\n            }\n            return !notEditableEl.closest(\"[data-snippet]\");\n        };\n        return (\n            !contentEditableEl.matches(\"input, [data-oe-readonly]\") &&\n            contentEditableEl.closest(\".o_editable\") &&\n            !isDescendantOfNotEditableNotSnippet(contentEditableEl)\n        );\n    }\n}\nregistry\n    .category(\"builder-plugins\")\n    .add(BuilderContentEditablePlugin.id, BuilderContentEditablePlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { isRemovable } from \"./remove_plugin\";\nimport { isClonable } from \"./clone_plugin\";\nimport { getElementsWithOption, isElementInViewport } from \"@html_builder/utils/utils\";\nimport { shouldEditableMediaBeEditable } from \"@html_builder/utils/utils_css\";\nimport { OptionsContainer } from \"@html_builder/sidebar/option_container\";\n\n/** @typedef {import(\"@html_builder/core/utils\").BaseOptionComponent} BaseOptionComponent */\n/** @typedef {import(\"@odoo/owl\").Component} Component */\n/** @typedef {import(\"plugins\").CSSSelector} CSSSelector */\n/** @typedef {import(\"plugins\").TranslatedString} TranslatedString */\n/**\n * @typedef {{\n *        class: string;\n *        title: TranslatedString;\n *        handler: () => Promise<void>;\n *        disabledReason?: string;\n * }} BuilderButtonDescriptor\n *\n * @typedef {{\n *     id: string;\n *     element: HTMLElement;\n *     options: BaseOptionComponent[];\n *     optionTitleComponents: BaseOptionComponent[] | [];\n *     headerMiddleButtons: Component[] | [];\n *     containerTitle: Component | {};\n *     hasOverlayOptions: boolean;\n *     isRemovable: boolean;\n *     removeDisabledReason: string;\n *     isClonable: boolean;\n *     cloneDisabledReason: string;\n *     optionsContainerTopButtons: BuilderButtonDescriptor[] | [];\n * }} BuilderOptionContainer\n */\n\n/**\n * @typedef { Object } BuilderOptionsShared\n * @property { BuilderOptionsPlugin['computeContainers'] } computeContainers\n * @property { BuilderOptionsPlugin['findOption'] } findOption\n * @property { BuilderOptionsPlugin['getContainers'] } getContainers\n * @property { BuilderOptionsPlugin['updateContainers'] } updateContainers\n * @property { BuilderOptionsPlugin['deactivateContainers'] } deactivateContainers\n * @property { BuilderOptionsPlugin['getTarget'] } getTarget\n * @property { BuilderOptionsPlugin['getPageContainers'] } getPageContainers\n * @property { BuilderOptionsPlugin['getRemoveDisabledReason'] } getRemoveDisabledReason\n * @property { BuilderOptionsPlugin['getCloneDisabledReason'] } getCloneDisabledReason\n * @property { BuilderOptionsPlugin['getReloadSelector'] } getReloadSelector\n * @property { BuilderOptionsPlugin['setNextTarget'] } setNextTarget\n * @property { BuilderOptionsPlugin['getBuilderOptionContext'] } getBuilderOptionContext\n */\n\n/**\n * @typedef {((containers: BuilderOptionContainer[]) => void)[]} change_current_options_containers_listeners\n * @typedef {((newTargetEl: HTMLElement) => void)[]} on_restore_containers_handlers\n *\n * @typedef {((el: HTMLElement) => [] | BuilderButtonDescriptor[])[]} get_options_container_top_buttons\n *\n * @typedef {{\n *     Component: Component;\n *     selector: CSSSelector;\n *     exclude: CSSSelector;\n *     applyTo: CSSSelector;\n *     props: object;\n *     editableOnly?: boolean;\n * }[]} builder_header_middle_buttons\n * @typedef {BaseOptionComponent[]} builder_options\n * @typedef {{\n *     selector: CSSSelector;\n *     getTitleExtraInfo: (editingElement: HTMLElement) => string;\n * }[]} container_title\n * @typedef {{\n *      Component: BaseOptionComponent;\n *      selector: CSSSelector;\n * }[]} elements_to_options_title_components\n * @typedef {{\n *      hasOption: (el: HTMLElement) => boolean;\n *      editableOnly?: boolean;\n * }[]} has_overlay_options\n * @typedef {CSSSelector[]} no_parent_containers\n * @typedef {((el: HTMLElement) => boolean)[]} keep_overlay_options\n */\n/**\n * @typedef {((arg: { el: HTMLElement, reasons: [] }) => void)[]} clone_disabled_reason_providers\n *\n * Appends new reasons to the `reasons` array given as a parameter.\n *\n * Example:\n *\n *     ({ el, reasons }) => {\n *         reasons.push(`I hate ${el.dataset.name}`);\n *     }\n */\n/**\n * @typedef {((arg: { el: HTMLElement, reasons: [] }) => void)[]} remove_disabled_reason_providers\n *\n * Appends new reasons to the `reasons` array given as a parameter.\n *\n * Example:\n *\n *     ({ el, reasons }) => {\n *         reasons.push(`I hate ${el.dataset.name}`);\n *     }\n */\n\nexport class BuilderOptionsPlugin extends Plugin {\n    static id = \"builderOptions\";\n    static dependencies = [\n        \"selection\",\n        \"overlay\",\n        \"operation\",\n        \"history\",\n        \"builderOverlay\",\n        \"overlayButtons\",\n    ];\n    static shared = [\n        \"computeContainers\",\n        \"findOption\",\n        \"getContainers\",\n        \"updateContainers\",\n        \"deactivateContainers\",\n        \"getTarget\",\n        \"getPageContainers\",\n        \"getRemoveDisabledReason\",\n        \"getCloneDisabledReason\",\n        \"getReloadSelector\",\n        \"setNextTarget\",\n        \"getBuilderOptionContext\",\n    ];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        before_add_step_handlers: this.onWillAddStep.bind(this),\n        step_added_handlers: this.onStepAdded.bind(this),\n        post_undo_handlers: (revertedStep) => this.restoreContainers(revertedStep, \"undo\"),\n        post_redo_handlers: (revertedStep) => this.restoreContainers(revertedStep, \"redo\"),\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        start_edition_handlers: () => {\n            if (this.config.initialTarget) {\n                const el = this.editable.querySelector(this.config.initialTarget);\n                this.updateContainers(el);\n            }\n        },\n    };\n\n    setup() {\n        this.builderOptions = this.getResource(\"builder_options\");\n        this.builderOptionsContext = new Map();\n        this.builderOptionsDependencies = new Map();\n        const options = this.builderOptions.concat([OptionsContainer]);\n        for (const Option of options) {\n            if (isLegacyOption(Option)) {\n                // Support legacy option definition.\n                continue;\n            }\n            this.getBuilderDependencies(Option);\n            this.getBuilderOptionContext(Option);\n        }\n\n        this.elementsToOptionsTitleComponents = withIds(\n            this.getResource(\"elements_to_options_title_components\")\n        );\n        // todo: remove that resource as we should be able to patch the class the normal way\n        this.getResource(\"patch_builder_options\").forEach((option) => {\n            this.patchBuilderOptions(option);\n        });\n        this.builderHeaderMiddleButtons = withIds(\n            this.getResource(\"builder_header_middle_buttons\")\n        );\n        this.builderContainerTitle = withIds(this.getResource(\"container_title\"));\n        // doing this manually instead of using addDomListener. This is because\n        // addDomListener will ignore all events from protected targets. But in\n        // our case, we still want to update the containers.\n        this.onClick = this.onClick.bind(this);\n        this.editable.addEventListener(\"click\", this.onClick, { capture: true });\n\n        this.lastContainers = [];\n\n        // Selector of elements that should not update/have containers when they\n        // are clicked.\n        this.notActivableElementsSelector = [\n            \"#web_editor-top-edit\",\n            \"#oe_manipulators\",\n            \".oe_drop_zone\",\n            \".o_notification_manager\",\n            \".o_we_no_overlay\",\n            \".ui-autocomplete\",\n            \".modal .btn-close\",\n            \".transfo-container\",\n            \".o_datetime_picker\",\n        ].join(\", \");\n    }\n\n    destroy() {\n        this.editable.removeEventListener(\"click\", this.onClick, { capture: true });\n    }\n\n    onClick(ev) {\n        this.dependencies.operation.next(() => {\n            this.updateContainers(ev.target);\n        });\n    }\n\n    getReloadSelector(editingElement) {\n        for (const container of [...this.lastContainers].reverse()) {\n            for (const option of container.options) {\n                if (option.reloadTarget) {\n                    return option.selector;\n                }\n            }\n        }\n        if (editingElement.closest(\"header\")) {\n            return \"header\";\n        }\n        if (editingElement.closest(\"main\")) {\n            return \"main\";\n        }\n        if (editingElement.closest(\"footer\")) {\n            return \"footer\";\n        }\n        return null;\n    }\n\n    updateContainers(target, { forceUpdate = false } = {}) {\n        if (this.dependencies.history.getIsCurrentStepModified()) {\n            console.warn(\n                \"Should not have any mutations in the current step when you update the container selection\"\n            );\n        }\n        if (this.dependencies.history.getIsPreviewing()) {\n            return;\n        }\n        if (target) {\n            if (target.closest(this.notActivableElementsSelector)) {\n                return;\n            }\n            this.target = target;\n        }\n        if (!this.target || !this.target.isConnected) {\n            const connectedContainers = this.lastContainers.filter((c) => c.element.isConnected);\n            this.target = connectedContainers.at(-1)?.element;\n        }\n\n        const newContainers = this.computeContainers(this.target);\n        // Do not update the containers if they did not change and are not\n        // forced to update.\n        if (\n            !forceUpdate &&\n            this.target?.isConnected &&\n            newContainers.length === this.lastContainers.length\n        ) {\n            const previousIds = this.lastContainers.map((c) => c.id);\n            const newIds = newContainers.map((c) => c.id);\n            const areSameElements = newIds.every((id, i) => id === previousIds[i]);\n            // Check if the overlay options status changed.\n            const previousOverlays = this.lastContainers.map((c) => c.hasOverlayOptions);\n            const newOverlays = newContainers.map((c) => c.hasOverlayOptions);\n            const areSameOverlays = previousOverlays.every((check, i) => check === newOverlays[i]);\n            if (areSameElements && areSameOverlays) {\n                const previousOptions = this.lastContainers.flatMap((c) => [\n                    ...c.options,\n                    ...c.headerMiddleButtons,\n                    c.containerTitle,\n                ]);\n                const newOptions = newContainers.flatMap((c) => [\n                    ...c.options,\n                    ...c.headerMiddleButtons,\n                    c.containerTitle,\n                ]);\n                const areSameOptions =\n                    newOptions.length === previousOptions.length &&\n                    newOptions.every((option, i) => option.id === previousOptions[i].id);\n                if (areSameOptions) {\n                    return;\n                }\n            }\n        }\n\n        this.lastContainers = newContainers;\n        this.dispatchTo(\"change_current_options_containers_listeners\", this.lastContainers);\n    }\n\n    getTarget() {\n        return this.target;\n    }\n\n    deactivateContainers() {\n        this.target = null;\n        this.lastContainers = [];\n        this.dispatchTo(\"change_current_options_containers_listeners\", this.lastContainers);\n    }\n\n    computeContainers(target) {\n        const mapElementsToOptions = (Options) => {\n            const map = new Map();\n            for (const Option of Options) {\n                const { selector, exclude, editableOnly } = Option;\n                let elements = getClosestElements(target, selector);\n                if (!elements.length) {\n                    continue;\n                }\n                elements = elements.filter((el) => checkElement(el, { exclude, editableOnly }));\n\n                for (const element of elements) {\n                    if (map.has(element)) {\n                        map.get(element).push(Option);\n                    } else {\n                        map.set(element, [Option]);\n                    }\n                }\n            }\n            return map;\n        };\n        const elementToOptions = mapElementsToOptions(this.builderOptions);\n        const elementToHeaderMiddleButtons = mapElementsToOptions(this.builderHeaderMiddleButtons);\n        const elementToContainerTitle = mapElementsToOptions(this.builderContainerTitle);\n        const elementToOptionTitleComponents = mapElementsToOptions(\n            this.elementsToOptionsTitleComponents\n        );\n\n        // Find the closest element with no options that should still have the\n        // overlay buttons.\n        let element = target;\n        while (element && !elementToOptions.has(element)) {\n            if (this.hasOverlayOptions(element)) {\n                elementToOptions.set(element, []);\n                break;\n            }\n            element = element.parentElement;\n        }\n\n        const previousElementToIdMap = new Map(this.lastContainers.map((c) => [c.element, c.id]));\n        let containers = [...elementToOptions]\n            .sort(([a], [b]) => (b.contains(a) ? 1 : -1))\n            .map(([element, options]) => ({\n                id: previousElementToIdMap.get(element) || uniqueId(),\n                element,\n                options,\n                optionTitleComponents: elementToOptionTitleComponents.get(element) || [],\n                headerMiddleButtons: elementToHeaderMiddleButtons.get(element) || [],\n                containerTitle: elementToContainerTitle.get(element)\n                    ? elementToContainerTitle.get(element)[0]\n                    : {},\n                hasOverlayOptions: this.hasOverlayOptions(element),\n                isRemovable: isRemovable(element),\n                removeDisabledReason: this.getRemoveDisabledReason(element),\n                isClonable: isClonable(element),\n                cloneDisabledReason: this.getCloneDisabledReason(element),\n                optionsContainerTopButtons: this.getOptionsContainerTopButtons(element),\n            }));\n        const lastValidContainerIdx = containers.findLastIndex((c) =>\n            this.getResource(\"no_parent_containers\").some((selector) => c.element.matches(selector))\n        );\n        if (lastValidContainerIdx > 0) {\n            containers = containers.slice(lastValidContainerIdx);\n        }\n        return containers;\n    }\n\n    getPageContainers() {\n        return this.computeContainers(this.editable.querySelector(\"main\"));\n    }\n\n    getContainers() {\n        return this.lastContainers;\n    }\n\n    hasOverlayOptions(el) {\n        // An inner snippet alone in a column should not have overlay options.\n        const parentEl = el.parentElement;\n        const isAloneInColumn = parentEl?.children.length === 1 && parentEl.matches(\".row > div\");\n        const isInnerSnippet = this.config.snippetModel.isInnerContent(el);\n        const keepOptions = this.delegateTo(\"keep_overlay_options\", el);\n        if (isInnerSnippet && isAloneInColumn && !keepOptions) {\n            return false;\n        }\n\n        for (const { hasOption, editableOnly } of this.getResource(\"has_overlay_options\")) {\n            if (checkElement(el, { editableOnly }) && hasOption(el)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    getOptionsContainerTopButtons(el) {\n        const buttons = [];\n        for (const getContainerButtons of this.getResource(\"get_options_container_top_buttons\")) {\n            buttons.push(...getContainerButtons(el));\n            for (const button of buttons) {\n                const handler = button.handler;\n                button.handler = (...args) => {\n                    this.dependencies.operation.next(async () => {\n                        await handler(...args);\n                        this.dependencies.history.addStep();\n                    });\n                };\n            }\n        }\n        return buttons;\n    }\n\n    cleanForSave({ root }) {\n        for (const Option of this.builderOptions) {\n            const { selector, exclude, cleanForSave } = Option;\n            if (!cleanForSave) {\n                continue;\n            }\n            for (const el of getElementsWithOption(root, selector, exclude)) {\n                const context = isLegacyOption(Option)\n                    ? undefined\n                    : this.getBuilderOptionContext(Option);\n                cleanForSave(el, context);\n            }\n        }\n    }\n\n    /**\n     * Activates the containers of the given element or deactivate them if false\n     * is given. They will be (de)activated once the current step is added (see\n     * `onStepAdded`).\n     *\n     * @param {HTMLElement|Boolean} targetEl the element to activate or `false`\n     */\n    setNextTarget(targetEl) {\n        if (this.dependencies.history.getIsPreviewing()) {\n            return;\n        }\n        // Store the next target to activate in the current step.\n        this.dependencies.history.setStepExtra(\"nextTarget\", targetEl);\n    }\n\n    onWillAddStep() {\n        // Store the current target in the current step.\n        this.dependencies.history.setStepExtra(\"currentTarget\", this.target);\n    }\n\n    onStepAdded({ step }) {\n        // If a target is specified, activate its containers, otherwise simply\n        // update them.\n        const nextTargetEl = step.extraStepInfos.nextTarget;\n        if (nextTargetEl) {\n            this.updateContainers(nextTargetEl, { forceUpdate: true });\n        } else if (nextTargetEl === false) {\n            this.deactivateContainers();\n        } else {\n            this.updateContainers();\n        }\n    }\n\n    /**\n     * Restores the containers of the target stored in the reverted step.\n     *\n     * @param {Object} revertedStep the step\n     * @param {String} mode \"undo\" or \"redo\"\n     */\n    restoreContainers(revertedStep, mode) {\n        if (revertedStep && revertedStep.extraStepInfos.currentTarget) {\n            let targetEl = revertedStep.extraStepInfos.currentTarget;\n            // If the step was supposed to activate another target, activate\n            // this one instead.\n            const nextTarget = revertedStep.extraStepInfos.nextTarget;\n            if (mode === \"redo\" && (nextTarget || nextTarget === false)) {\n                targetEl = nextTarget;\n            }\n            if (targetEl) {\n                this.dispatchTo(\"on_restore_containers_handlers\", targetEl);\n                this.updateContainers(targetEl, { forceUpdate: true });\n                // Scroll to the target if not visible.\n                if (!isElementInViewport(targetEl)) {\n                    targetEl.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n                }\n            } else {\n                this.deactivateContainers();\n            }\n        }\n    }\n\n    getRemoveDisabledReason(el) {\n        const reasons = [];\n        this.dispatchTo(\"remove_disabled_reason_providers\", { el, reasons });\n        return reasons.length ? reasons.join(\" \") : undefined;\n    }\n\n    getCloneDisabledReason(el) {\n        const reasons = [];\n        this.dispatchTo(\"clone_disabled_reason_providers\", { el, reasons });\n        return reasons.length ? reasons.join(\" \") : undefined;\n    }\n\n    patchBuilderOptions({ target_name, target_element, method, value }) {\n        if (!target_name || !target_element || !method || (!value && method !== \"remove\")) {\n            throw new Error(\n                `Missing patch_builder_options required parameters: target_name, target_element, method, value`\n            );\n        }\n\n        const builderOption = this.builderOptions.find((option) => option.name === target_name);\n        if (!builderOption) {\n            throw new Error(`Builder option ${target_name} not found`);\n        }\n\n        switch (method) {\n            case \"replace\":\n                builderOption[target_element] = value;\n                break;\n            case \"remove\":\n                delete builderOption[target_element];\n                break;\n            case \"add\":\n                if (!builderOption[target_element]) {\n                    throw new Error(\n                        `Builder option ${target_name} does not have ${target_element}`\n                    );\n                }\n                builderOption[target_element] += `, ${value}`;\n                break;\n            default:\n                throw new Error(`Unknown method ${method}`);\n        }\n    }\n\n    /**\n     * Finds the given option in the given element closest options container, as\n     * well as in the parent containers if specified, and returns it and its\n     * target element.\n     *\n     * @param {HTMLElement} el the element\n     * @param {String} optionName the option name\n     * @param {Boolean} [allowParent=false] true if the parent containers should\n     *   be considered\n     * @returns {Object} - `option`: the requested option\n     *                   - `targetEl`: the target element of the option\n     */\n    findOption(el, optionName, allowParent = false) {\n        let containers = this.getContainers().filter((container) => container.element.contains(el));\n        containers.reverse();\n        if (!allowParent) {\n            containers = [containers[0]];\n        }\n\n        // Find the given option in the active containers and the element on\n        // which it applies.\n        let targetEl, requestedOption;\n        for (const { element, options } of containers) {\n            requestedOption = options.find((option) => {\n                if (option.OptionComponent) {\n                    return option.OptionComponent.name === optionName;\n                } else {\n                    return option.template.split(\".\").at(-1) === optionName;\n                }\n            });\n            if (requestedOption) {\n                const { applyTo } = requestedOption;\n                targetEl = applyTo ? element.querySelector(applyTo) : element;\n                break;\n            }\n        }\n\n        return { option: requestedOption, targetEl };\n    }\n    /**\n     * Get all dependencies of an OptionComponent and all its descendants.\n     */\n    getBuilderDependencies(OptionComponent) {\n        const cachedDeps = this.builderOptionsDependencies.get(OptionComponent);\n        if (cachedDeps) {\n            return cachedDeps;\n        }\n        const deps = OptionComponent.dependencies || [];\n        this.builderOptionsDependencies.set(OptionComponent, deps);\n        const childDeps = Object.values(OptionComponent.components || {}).flatMap(\n            this.getBuilderDependencies.bind(this)\n        );\n        deps.push(...childDeps);\n        return deps;\n    }\n    /**\n     * Get all the methods (window, document, getResources, ...) that are available in plugins. Provide the OptionComponent to set the right dependencies.\n     */\n    getBuilderOptionContext(OptionComponent) {\n        const context = this.builderOptionsContext.get(OptionComponent);\n        if (!context) {\n            const deps = this.getBuilderDependencies(OptionComponent);\n            const context = this.__editor.getEditorContext(deps);\n            this.builderOptionsContext.set(OptionComponent, context);\n            return context;\n        }\n        return context;\n    }\n}\n\nfunction getClosestElements(element, selector) {\n    if (!element) {\n        // TODO we should remove it\n        return [];\n    }\n    const parent = element.closest(selector);\n    return parent ? [parent, ...getClosestElements(parent.parentElement, selector)] : [];\n}\n\n/**\n * Checks if the given element is valid in order to have an option.\n *\n * @param {HTMLElement} el\n * @param {Boolean} editableOnly when set to false, the element does not need to\n *     be in an editable area and the checks are therefore lighter.\n *     (= previous data-no-check/noCheck)\n * @param {String} exclude\n * @returns {Boolean}\n */\nexport function checkElement(el, { editableOnly = true, exclude = \"\" }) {\n    // Unless specified otherwise, the element should be in an editable.\n    if (editableOnly && !el.closest(\".o_editable\")) {\n        return false;\n    }\n    // Check that the element is not to be excluded.\n    exclude += `${exclude && \", \"}.o_snippet_not_selectable`;\n    if (el.matches(exclude)) {\n        return false;\n    }\n    // If an editable is not required, do not check anything else.\n    if (!editableOnly) {\n        return true;\n    }\n    // `o_editable_media` bypasses the `o_not_editable` class.\n    if (el.matches(\".o_editable_media\")) {\n        return shouldEditableMediaBeEditable(el);\n    }\n    return !el.matches('.o_not_editable:not(.s_social_media) :not([contenteditable=\"true\"])');\n}\n\nfunction withIds(arr) {\n    return arr.map((el) => ({ ...el, id: uniqueId() }));\n}\n\nexport function isLegacyOption(option) {\n    return typeof option === \"object\";\n}\n", "import { OptionsContainer } from \"@html_builder/sidebar/option_container\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { BuilderOptionsPlugin } from \"./builder_options_plugin\";\n\n/** @typedef {import(\"./builder_options_plugin\").BuilderOptionContainer[]} translate_options */\n\nexport class BuilderOptionsTranslationPlugin extends Plugin {\n    static id = \"builderOptions\";\n    static shared = [\n        \"deactivateContainers\",\n        \"getTarget\",\n        \"updateContainers\",\n        \"setNextTarget\",\n        \"getBuilderOptionContext\",\n    ];\n    static dependencies = [\"history\"];\n\n    setup() {\n        this.builderOptions = this.getResource(\"translate_options\");\n        this.builderOptionsContext = new Map();\n        this.builderOptionsDependencies = new Map();\n        const options = this.builderOptions.concat([OptionsContainer]);\n        for (const Option of options) {\n            this.getBuilderDependencies(Option);\n            this.getBuilderOptionContext(Option);\n        }\n    }\n    deactivateContainers() {}\n    getTarget() {}\n    updateContainers() {}\n    setNextTarget(targetEl) {\n        // Store the next target to activate in the current step.\n        this.dependencies.history.setStepExtra(\"nextTarget\", targetEl);\n    }\n}\n\nBuilderOptionsTranslationPlugin.prototype.getBuilderDependencies =\n    BuilderOptionsPlugin.prototype.getBuilderDependencies;\nBuilderOptionsTranslationPlugin.prototype.getBuilderOptionContext =\n    BuilderOptionsPlugin.prototype.getBuilderOptionContext;\n", "import { renderToElement } from \"@web/core/utils/render\";\nimport {\n    addBackgroundGrid,\n    getGridProperties,\n    getGridItemProperties,\n    resizeGrid,\n    setElementToMaxZindex,\n} from \"@html_builder/utils/grid_layout_utils\";\n\n// TODO move them elsewhere.\nexport const sizingY = {\n    selector: \"section, .row > div, .parallax, .s_hr, .carousel-item, .s_rating\",\n    exclude:\n        \"section:has(> .carousel), .s_image_gallery .carousel-item, .s_col_no_resize.row > div, .s_col_no_resize\",\n};\nexport const sizingX = {\n    selector: \".row > div\",\n    exclude: \".s_col_no_resize.row > div, .s_col_no_resize\",\n};\nexport const sizingGrid = {\n    selector: \".row > div\",\n    exclude: \".s_col_no_resize.row > div, .s_col_no_resize\",\n};\n\nexport class BuilderOverlay {\n    constructor(\n        overlayTarget,\n        {\n            iframe,\n            overlayContainer,\n            history,\n            hasOverlayOptions,\n            next,\n            isMobileView,\n            mobileBreakpoint,\n            isRtl,\n        }\n    ) {\n        this.history = history;\n        this.next = next;\n        this.hasOverlayOptions = hasOverlayOptions;\n        this.iframe = iframe;\n        this.overlayContainer = overlayContainer;\n        this.overlayElement = renderToElement(\"html_builder.BuilderOverlay\");\n        this.overlayTarget = overlayTarget;\n        this.hasSizingHandles = this.hasSizingHandles();\n        this.handlesWrapperEl = this.overlayElement.querySelector(\".o_handles\");\n        this.handleEls = this.overlayElement.querySelectorAll(\".o_handle\");\n        // Avoid \"querySelectoring\" the handles every time.\n        this.yHandles = this.handlesWrapperEl.querySelectorAll(\n            `.n:not(.o_grid_handle), .s:not(.o_grid_handle)`\n        );\n        this.xHandles = this.handlesWrapperEl.querySelectorAll(\n            `.e:not(.o_grid_handle), .w:not(.o_grid_handle)`\n        );\n        this.gridHandles = this.handlesWrapperEl.querySelectorAll(\".o_grid_handle\");\n        this.isMobileView = isMobileView;\n        this.mobileBreakpoint = mobileBreakpoint;\n        this.isRtl = isRtl;\n\n        this.initHandles();\n        this.initSizing();\n        this.refreshHandles();\n    }\n\n    hasSizingHandles() {\n        if (!this.hasOverlayOptions) {\n            return false;\n        }\n        return this.isResizableY() || this.isResizableX() || this.isResizableGrid();\n    }\n\n    // displayOverlayOptions(el) {\n    //     // TODO when options will be more clear:\n    //     // - moving\n    //     // - timeline\n    //     // (maybe other where `displayOverlayOptions: true`)\n    // }\n\n    isActive() {\n        // TODO active still necessary ? (check when we have preview mode)\n        return this.overlayElement.matches(\".oe_active, .o_we_overlay_preview\");\n    }\n\n    refreshPosition() {\n        if (!this.isActive()) {\n            return;\n        }\n\n        const openModalEl = this.overlayTarget.querySelector(\".modal.show\");\n        const overlayTarget = openModalEl ? openModalEl : this.overlayTarget;\n        // TODO transform\n        const iframeRect = this.iframe.getBoundingClientRect();\n        const overlayContainerRect = this.overlayContainer.getBoundingClientRect();\n        const targetRect = overlayTarget.getBoundingClientRect();\n        Object.assign(this.overlayElement.style, {\n            width: `${targetRect.width}px`,\n            height: `${targetRect.height}px`,\n            top: `${iframeRect.y + targetRect.y - overlayContainerRect.y + window.scrollY}px`,\n            left: `${iframeRect.x + targetRect.x - overlayContainerRect.x + window.scrollX}px`,\n        });\n        this.handlesWrapperEl.style.height = `${targetRect.height}px`;\n    }\n\n    refreshHandles() {\n        if (!this.hasSizingHandles || !this.isActive()) {\n            return;\n        }\n\n        if (this.overlayTarget.parentNode?.classList.contains(\"row\")) {\n            const isMobile = this.isMobileView(this.overlayTarget);\n            const isGridOn = this.overlayTarget.classList.contains(\"o_grid_item\");\n            const isGrid = !isMobile && isGridOn;\n            // Hiding/showing the correct resize handles if we are in grid mode\n            // or not.\n            this.handleEls.forEach((handleEl) => {\n                const isGridHandle = handleEl.classList.contains(\"o_grid_handle\");\n                handleEl.classList.toggle(\"d-none\", isGrid ^ isGridHandle);\n                // Disabling the vertical resize if we are in mobile view.\n                const isVerticalSizing = handleEl.matches(\".n, .s\");\n                handleEl.classList.toggle(\"readonly\", isMobile && isVerticalSizing && isGridOn);\n            });\n        }\n\n        this.updateHandleY();\n    }\n\n    toggleOverlay(show) {\n        this.overlayElement.classList.toggle(\"oe_active\", show);\n        this.refreshPosition();\n        this.refreshHandles();\n    }\n\n    toggleOverlayPreview(show) {\n        this.overlayElement.classList.toggle(\"o_we_overlay_preview\", show);\n        this.refreshPosition();\n        this.refreshHandles();\n    }\n\n    toggleOverlayVisibility(show) {\n        if (!this.isActive()) {\n            return;\n        }\n        this.overlayElement.classList.toggle(\"o_overlay_hidden\", !show);\n    }\n\n    destroy() {\n        if (!this.hasSizingHandles) {\n            return;\n        }\n\n        this.handleEls.forEach((handleEl) =>\n            handleEl.removeEventListener(\"pointerdown\", this._onSizingStart)\n        );\n    }\n\n    //--------------------------------------------------------------------------\n    // Sizing\n    //--------------------------------------------------------------------------\n\n    isResizableY() {\n        return (\n            this.overlayTarget.matches(sizingY.selector) &&\n            !this.overlayTarget.matches(sizingY.exclude)\n        );\n    }\n\n    isResizableX() {\n        return (\n            this.overlayTarget.matches(sizingX.selector) &&\n            !this.overlayTarget.matches(sizingX.exclude)\n        );\n    }\n\n    isResizableGrid() {\n        return (\n            this.overlayTarget.matches(sizingGrid.selector) &&\n            !this.overlayTarget.matches(sizingGrid.exclude)\n        );\n    }\n\n    initHandles() {\n        if (!this.hasSizingHandles) {\n            return;\n        }\n        if (this.isResizableY()) {\n            this.yHandles.forEach((handleEl) => handleEl.classList.remove(\"readonly\"));\n        }\n        if (this.isResizableX()) {\n            this.xHandles.forEach((handleEl) => handleEl.classList.remove(\"readonly\"));\n        }\n        if (this.isResizableGrid()) {\n            this.gridHandles.forEach((handleEl) => handleEl.classList.remove(\"readonly\"));\n        }\n    }\n\n    initSizing() {\n        if (!this.hasSizingHandles) {\n            return;\n        }\n\n        this._onSizingStart = this.onSizingStart.bind(this);\n        this.handleEls.forEach((handleEl) =>\n            handleEl.addEventListener(\"pointerdown\", this._onSizingStart)\n        );\n    }\n\n    replaceSizingClass(classRegex, newClass) {\n        const newClassName = (this.overlayTarget.className || \"\").replace(classRegex, \"\");\n        this.overlayTarget.className = newClassName;\n        this.overlayTarget.classList.add(newClass);\n    }\n\n    getSizingYConfig() {\n        const isTargetHR = this.overlayTarget.matches(\"hr\");\n        const nClass = isTargetHR ? \"mt\" : \"pt\";\n        const nProperty = isTargetHR ? \"margin-top\" : \"padding-top\";\n        const sClass = isTargetHR ? \"mb\" : \"pb\";\n        const sProperty = isTargetHR ? \"margin-bottom\" : \"padding-bottom\";\n\n        const values = [0, 4];\n        for (let i = 1; i <= 256 / 8; i++) {\n            values.push(i * 8);\n        }\n\n        return {\n            n: { classes: values.map((v) => nClass + v), values: values, cssProperty: nProperty },\n            s: { classes: values.map((v) => sClass + v), values: values, cssProperty: sProperty },\n        };\n    }\n\n    onResizeY(compass, initialClasses, currentIndex) {\n        this.updateHandleY();\n    }\n\n    updateHandleY() {\n        this.yHandles.forEach((handleEl) => {\n            const topOrBottom = handleEl.matches(\".n\") ? \"top\" : \"bottom\";\n            const padding = window.getComputedStyle(this.overlayTarget)[`padding-${topOrBottom}`];\n            handleEl.style.height = padding; // TODO outerHeight (deduce borders ?)\n        });\n    }\n\n    getSizingXConfig() {\n        const resolutionModifier = this.isMobile ? \"\" : `${this.mobileBreakpoint}-`;\n        const rowWidth = this.overlayTarget.closest(\".row\").getBoundingClientRect().width;\n        const valuesE = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];\n        const valuesW = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];\n        return {\n            e: {\n                classes: valuesE.map((v) => `col-${resolutionModifier}${v}`),\n                values: valuesE.map((v) => (rowWidth / 12) * v),\n                cssProperty: \"width\",\n            },\n            w: {\n                classes: valuesW.map((v) => `offset-${resolutionModifier}${v}`),\n                values: valuesW.map((v) => (rowWidth / 12) * v),\n                cssProperty: \"margin-left\",\n            },\n        };\n    }\n\n    onResizeX(compass, initialClasses, currentIndex) {\n        const resolutionModifier = this.isMobile ? \"\" : `${this.mobileBreakpoint}-`;\n        // (?!\\S): following char cannot be a non-space character\n        const offsetRegex = new RegExp(`(?:^|\\\\s+)offset-${resolutionModifier}(\\\\d{1,2})(?!\\\\S)`);\n        const colRegex = new RegExp(`(?:^|\\\\s+)col-${resolutionModifier}(\\\\d{1,2})(?!\\\\S)`);\n\n        const initialOffset = Number(initialClasses.match(offsetRegex)?.[1] || 0);\n\n        if (compass === \"w\") {\n            // Replacing the col class so the right border does not move when we\n            // change the offset.\n            const initialCol = Number(initialClasses.match(colRegex)?.[1] || 12);\n            let offset = Number(this.overlayTarget.className.match(offsetRegex)?.[1] || 0);\n            const offsetClass = `offset-${resolutionModifier}${offset}`;\n\n            let colSize = initialCol - (offset - initialOffset);\n            if (colSize <= 0) {\n                colSize = 1;\n                offset = initialOffset + initialCol - 1;\n            }\n            this.overlayTarget.classList.remove(offsetClass);\n            this.replaceSizingClass(colRegex, `col-${resolutionModifier}${colSize}`);\n            if (offset > 0) {\n                this.overlayTarget.classList.add(`offset-${resolutionModifier}${offset}`);\n            }\n\n            // Add/remove the `offset-lg-0` class when needed.\n            if (this.isMobile && offset === 0) {\n                this.overlayTarget.classList.remove(`offset-${this.mobileBreakpoint}-0`);\n            } else {\n                const className = this.overlayTarget.className;\n                const hasDesktopClass = !!className.match(\n                    new RegExp(`(^|\\\\s+)offset-${this.mobileBreakpoint}-\\\\d{1,2}(?!\\\\S)`)\n                );\n                const hasMobileClass = !!className.match(/(^|\\s+)offset-\\d{1,2}(?!\\S)/);\n                if (\n                    (this.isMobile && offset > 0 && !hasDesktopClass) ||\n                    (!this.isMobile && offset === 0 && hasMobileClass)\n                ) {\n                    this.overlayTarget.classList.add(`offset-${this.mobileBreakpoint}-0`);\n                }\n            }\n        } else if (initialOffset > 0) {\n            const col = Number(this.overlayTarget.className.match(colRegex)?.[1] || 0);\n            // Avoid overflowing to the right if the column size + the offset\n            // exceeds 12.\n            if (col + initialOffset > 12) {\n                this.replaceSizingClass(colRegex, `col-${resolutionModifier}${12 - initialOffset}`);\n            }\n        }\n    }\n\n    getSizingGridConfig() {\n        const rowEl = this.overlayTarget.closest(\".row\");\n        const gridProp = getGridProperties(rowEl);\n        const { rowStart, rowEnd, columnStart, columnEnd } = getGridItemProperties(\n            this.overlayTarget\n        );\n\n        const valuesN = [];\n        const valuesS = [];\n        for (let i = 1; i < parseInt(rowEnd) + 12; i++) {\n            valuesN.push(i);\n            valuesS.push(i + 1);\n        }\n        const valuesW = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];\n        const valuesE = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];\n\n        return {\n            n: {\n                classes: valuesN.map((v) => \"g-height-\" + (rowEnd - v)),\n                values: valuesN.map((v) => (gridProp.rowSize + gridProp.rowGap) * (v - 1)),\n                cssProperty: \"grid-row-start\",\n            },\n            s: {\n                classes: valuesS.map((v) => \"g-height-\" + (v - rowStart)),\n                values: valuesS.map((v) => (gridProp.rowSize + gridProp.rowGap) * (v - 1)),\n                cssProperty: \"grid-row-end\",\n            },\n            w: {\n                classes: valuesW.map((v) => `g-col-${this.mobileBreakpoint}-` + (columnEnd - v)),\n                values: valuesW.map((v) => (gridProp.columnSize + gridProp.columnGap) * (v - 1)),\n                cssProperty: \"grid-column-start\",\n            },\n            e: {\n                classes: valuesE.map((v) => `g-col-${this.mobileBreakpoint}-` + (v - columnStart)),\n                values: valuesE.map((v) => (gridProp.columnSize + gridProp.columnGap) * (v - 1)),\n                cssProperty: \"grid-column-end\",\n            },\n        };\n    }\n\n    onResizeGrid(compass, initialClasses, currentIndex) {\n        const style = this.overlayTarget.style;\n        if (compass === \"n\") {\n            const rowEnd = parseInt(style.gridRowEnd);\n            if (currentIndex < 0) {\n                style.gridRowStart = 1;\n            } else if (currentIndex + 1 >= rowEnd) {\n                style.gridRowStart = rowEnd - 1;\n            } else {\n                style.gridRowStart = currentIndex + 1;\n            }\n        } else if (compass === \"s\") {\n            const rowStart = parseInt(style.gridRowStart);\n            const rowEnd = parseInt(style.gridRowEnd);\n            if (currentIndex + 2 <= rowStart) {\n                style.gridRowEnd = rowStart + 1;\n            } else {\n                style.gridRowEnd = currentIndex + 2;\n            }\n\n            // Updating the grid height.\n            const rowEl = this.overlayTarget.parentNode;\n            const rowCount = parseInt(rowEl.dataset.rowCount);\n            const backgroundGridEl = rowEl.querySelector(\".o_we_background_grid\");\n            const backgroundGridRowEnd = parseInt(backgroundGridEl.style.gridRowEnd);\n            let rowMove = 0;\n            if (style.gridRowEnd > rowEnd && style.gridRowEnd > rowCount + 1) {\n                rowMove = style.gridRowEnd - rowEnd;\n            } else if (style.gridRowEnd < rowEnd && style.gridRowEnd >= rowCount + 1) {\n                rowMove = style.gridRowEnd - rowEnd;\n            }\n            backgroundGridEl.style.gridRowEnd = backgroundGridRowEnd + rowMove;\n        } else if (compass === \"w\") {\n            const columnEnd = parseInt(style.gridColumnEnd);\n            if (currentIndex < 0) {\n                style.gridColumnStart = 1;\n            } else if (currentIndex + 1 >= columnEnd) {\n                style.gridColumnStart = columnEnd - 1;\n            } else {\n                style.gridColumnStart = currentIndex + 1;\n            }\n        } else if (compass === \"e\") {\n            const columnStart = parseInt(style.gridColumnStart);\n            if (currentIndex + 2 > 13) {\n                style.gridColumnEnd = 13;\n            } else if (currentIndex + 2 <= columnStart) {\n                style.gridColumnEnd = columnStart + 1;\n            } else {\n                style.gridColumnEnd = currentIndex + 2;\n            }\n        }\n\n        if (compass === \"n\" || compass === \"s\") {\n            const numberRows = style.gridRowEnd - style.gridRowStart;\n            this.replaceSizingClass(/\\s*(g-height-)([0-9-]+)/g, `g-height-${numberRows}`);\n        }\n\n        if (compass === \"w\" || compass === \"e\") {\n            const numberColumns = style.gridColumnEnd - style.gridColumnStart;\n            this.replaceSizingClass(\n                new RegExp(`\\\\s*(g-col-${this.mobileBreakpoint}-)([0-9-]+)`, \"g\"),\n                `g-col-${this.mobileBreakpoint}-${numberColumns}`\n            );\n        }\n    }\n\n    getDirections(ev, handleEl, sizingConfig) {\n        let compass = false;\n        let XY = false;\n        if (handleEl.matches(\".n\")) {\n            compass = \"n\";\n            XY = \"Y\";\n        } else if (handleEl.matches(\".s\")) {\n            compass = \"s\";\n            XY = \"Y\";\n        } else if (handleEl.matches(\".e\")) {\n            compass = \"e\";\n            XY = \"X\";\n        } else if (handleEl.matches(\".w\")) {\n            compass = \"w\";\n            XY = \"X\";\n        } else if (handleEl.matches(\".nw\")) {\n            compass = \"nw\";\n            XY = \"YX\";\n        } else if (handleEl.matches(\".ne\")) {\n            compass = \"ne\";\n            XY = \"YX\";\n        } else if (handleEl.matches(\".sw\")) {\n            compass = \"sw\";\n            XY = \"YX\";\n        } else if (handleEl.matches(\".se\")) {\n            compass = \"se\";\n            XY = \"YX\";\n        }\n\n        if (this.isRtl) {\n            if (compass.includes(\"e\")) {\n                compass = compass.replace(\"e\", \"w\");\n            } else if (compass.includes(\"w\")) {\n                compass = compass.replace(\"w\", \"e\");\n            }\n        }\n\n        const currentConfig = [];\n        for (let i = 0; i < compass.length; i++) {\n            currentConfig.push(sizingConfig[compass[i]]);\n        }\n\n        const directions = [];\n        for (const [i, config] of currentConfig.entries()) {\n            // Compute the current index based on the current class/style.\n            let currentIndex = 0;\n            const cssProperty = config.cssProperty;\n            const cssPropertyValue = parseInt(\n                window.getComputedStyle(this.overlayTarget)[cssProperty]\n            );\n            config.classes.forEach((c, index) => {\n                if (this.overlayTarget.classList.contains(c)) {\n                    currentIndex = index;\n                } else if (config.values[index] === cssPropertyValue) {\n                    currentIndex = index;\n                }\n            });\n\n            directions.push({\n                config,\n                currentIndex,\n                initialIndex: currentIndex,\n                initialClasses: this.overlayTarget.className,\n                classRegex: new RegExp(\n                    \"\\\\s*\" + config.classes[currentIndex].replace(/[-]*[0-9]+/, \"[-]*[0-9]+\"),\n                    \"g\"\n                ),\n                initialPageXY: ev[\"page\" + XY[i]],\n                XY: XY[i],\n                compass: compass[i],\n            });\n        }\n\n        return directions;\n    }\n\n    onSizingStart(ev) {\n        ev.preventDefault();\n        const pointerDownTime = ev.timeStamp;\n\n        // Lock the mutex.\n        let sizingResolve;\n        const sizingProm = new Promise((resolve) => (sizingResolve = () => resolve()));\n        this.next(async () => await sizingProm, { withLoadingEffect: false });\n        const cancelSizing = this.history.makeSavePoint();\n\n        const handleEl = ev.currentTarget;\n        const isGridHandle = handleEl.classList.contains(\"o_grid_handle\");\n        this.isMobile = this.isMobileView(this.overlayTarget);\n\n        // If we are in grid mode, add a background grid and place it in front\n        // of the other elements.\n        let rowEl, backgroundGridEl;\n        if (isGridHandle) {\n            rowEl = this.overlayTarget.parentNode;\n            backgroundGridEl = addBackgroundGrid(rowEl, 0);\n            setElementToMaxZindex(backgroundGridEl, rowEl);\n        }\n\n        let sizingConfig, onResize;\n        if (isGridHandle) {\n            sizingConfig = this.getSizingGridConfig();\n            onResize = this.onResizeGrid.bind(this);\n        } else if (handleEl.matches(\".n, .s\")) {\n            sizingConfig = this.getSizingYConfig();\n            onResize = this.onResizeY.bind(this);\n        } else {\n            sizingConfig = this.getSizingXConfig();\n            onResize = this.onResizeX.bind(this);\n        }\n\n        const directions = this.getDirections(ev, handleEl, sizingConfig);\n\n        // Set the cursor.\n        const cursorClass = `${window.getComputedStyle(handleEl)[\"cursor\"]}-important`;\n        window.document.body.classList.add(cursorClass);\n        // Prevent the iframe from absorbing the pointer events.\n        const iframeEl = this.overlayTarget.ownerDocument.defaultView.frameElement;\n        iframeEl.classList.add(\"o_resizing\");\n\n        this.overlayElement.classList.remove(\"o_handlers_idle\");\n\n        const onSizingMove = (ev) => {\n            for (const dir of directions) {\n                const configValues = dir.config.values;\n                const currentIndex = dir.currentIndex;\n                const currentValue = configValues[currentIndex];\n\n                // Get the number of pixels by which the pointer moved, compared\n                // to the initial position of the handle.\n                let deltaRaw = ev[`page${dir.XY}`] - dir.initialPageXY;\n                // In RTL mode, reverse only horizontal movement (X axis).\n                if (dir.XY === \"X\" && this.isRtl) {\n                    deltaRaw = -deltaRaw;\n                }\n                const delta = deltaRaw + configValues[dir.initialIndex];\n\n                // Compute the indexes of the next step and the step before it,\n                // based on the delta.\n                let nextIndex, beforeIndex;\n                if (delta > currentValue) {\n                    const nextValue = configValues.find((v) => v > delta);\n                    nextIndex = nextValue\n                        ? configValues.indexOf(nextValue)\n                        : configValues.length - 1;\n                    beforeIndex = nextIndex > 0 ? nextIndex - 1 : currentIndex;\n                } else if (delta < currentValue) {\n                    const nextValue = configValues.findLast((v) => v < delta);\n                    nextIndex = nextValue ? configValues.indexOf(nextValue) : 0;\n                    beforeIndex =\n                        nextIndex < configValues.length - 1 ? nextIndex + 1 : currentIndex;\n                }\n\n                let change = false;\n                if (delta !== currentValue) {\n                    // First, catch up with the pointer (in the case we moved\n                    // really fast).\n                    if (beforeIndex !== currentIndex) {\n                        this.replaceSizingClass(dir.classRegex, dir.config.classes[beforeIndex]);\n                        dir.currentIndex = beforeIndex;\n                        change = true;\n                    }\n                    // If the pointer moved by at least 2/3 of the space between\n                    // the current and the next step, the handle is snapped to\n                    // the next step and the class is replaced by the one\n                    // matching this step.\n                    const threshold =\n                        (2 * configValues[nextIndex] + configValues[dir.currentIndex]) / 3;\n                    if (\n                        (delta > currentValue && delta > threshold) ||\n                        (delta < currentValue && delta < threshold)\n                    ) {\n                        this.replaceSizingClass(dir.classRegex, dir.config.classes[nextIndex]);\n                        dir.currentIndex = nextIndex;\n                        change = true;\n                    }\n                }\n\n                if (change) {\n                    onResize(dir.compass, dir.initialClasses, dir.currentIndex);\n                    // TODO notify other options (e.g. steps)\n                }\n            }\n        };\n\n        const onSizingStop = (ev) => {\n            ev.preventDefault();\n            window.removeEventListener(\"pointermove\", onSizingMove);\n            window.removeEventListener(\"pointerup\", onSizingStop);\n            window.document.body.classList.remove(cursorClass);\n            iframeEl.classList.remove(\"o_resizing\");\n            this.overlayElement.classList.add(\"o_handlers_idle\");\n\n            // If we are in grid mode, removes the background grid.\n            // Also sync the col-* class with the g-col-* class so the\n            // toggle to normal mode and the mobile view are well done.\n            if (isGridHandle) {\n                backgroundGridEl.remove();\n                resizeGrid(rowEl);\n\n                const colClass = [...this.overlayTarget.classList].find((c) => /^col-/.test(c));\n                const gColClass = [...this.overlayTarget.classList].find((c) => /^g-col-/.test(c));\n                this.overlayTarget.classList.remove(colClass);\n                this.overlayTarget.classList.add(gColClass.substring(2));\n            }\n\n            // Cancel the sizing if the element was not resized (to not have\n            // mutations).\n            const wasResized = !directions.every((dir) => dir.initialIndex === dir.currentIndex);\n            if (wasResized) {\n                this.history.addStep();\n            } else {\n                cancelSizing();\n            }\n\n            // Free the mutex.\n            sizingResolve();\n\n            // If no resizing happened and if the pointer was down less than\n            // 500 ms, we assume that the user wanted to click on the element\n            // behind the handle.\n            if (!wasResized) {\n                const pointerUpTime = ev.timeStamp;\n                const pointerDownDuration = pointerUpTime - pointerDownTime;\n                if (pointerDownDuration < 500) {\n                    // Find the first element behind the overlay.\n                    const sameCoordinatesEls = this.overlayTarget.ownerDocument.elementsFromPoint(\n                        ev.pageX,\n                        ev.pageY\n                    );\n                    // Check if it has native JS `click` function\n                    const toBeClickedEl = sameCoordinatesEls.find(\n                        (el) =>\n                            !this.overlayContainer.contains(el) &&\n                            !el.matches(\".o_loading_screen\") &&\n                            typeof el.click === \"function\"\n                    );\n                    if (toBeClickedEl) {\n                        toBeClickedEl.click();\n                    }\n                }\n            }\n        };\n\n        window.addEventListener(\"pointermove\", onSizingMove);\n        window.addEventListener(\"pointerup\", onSizingStop);\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { getScrollingElement, getScrollingTarget } from \"@web/core/utils/scrolling\";\nimport { checkElement } from \"../builder_options_plugin\";\nimport { BuilderOverlay, sizingY, sizingX, sizingGrid } from \"./builder_overlay\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\nfunction isResizable(el) {\n    const isResizableY = el.matches(sizingY.selector) && !el.matches(sizingY.exclude);\n    const isResizableX = el.matches(sizingX.selector) && !el.matches(sizingX.exclude);\n    const isResizableGrid = el.matches(sizingGrid.selector) && !el.matches(sizingGrid.exclude);\n    return isResizableY || isResizableX || isResizableGrid;\n}\n\n/**\n * @typedef { Object } BuilderOverlayShared\n * @property { BuilderOverlayPlugin['showOverlayPreview'] } showOverlayPreview\n * @property { BuilderOverlayPlugin['hideOverlayPreview'] } hideOverlayPreview\n * @property { BuilderOverlayPlugin['refreshOverlays'] } refreshOverlays\n */\n\nexport class BuilderOverlayPlugin extends Plugin {\n    static id = \"builderOverlay\";\n    static dependencies = [\"localOverlay\", \"history\", \"operation\"];\n    static shared = [\"showOverlayPreview\", \"hideOverlayPreview\", \"refreshOverlays\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        step_added_handlers: this.refreshOverlays.bind(this),\n        change_current_options_containers_listeners: this.openBuilderOverlays.bind(this),\n        on_mobile_preview_clicked: withSequence(20, this.refreshOverlays.bind(this)),\n        has_overlay_options: { hasOption: (el) => isResizable(el) },\n    };\n\n    setup() {\n        // TODO find how to not overflow the mobile preview.\n        this.iframe = this.editable.ownerDocument.defaultView.frameElement;\n        this.overlayContainer = this.dependencies.localOverlay.makeLocalOverlay(\n            \"builder-overlay-container\"\n        );\n        // If the user scrolls the mouse wheel while hovering overlayContainer,\n        // no scroll will happen to the page. We need to manually process\n        // wheel events happening on overlayContainer.\n        this.overlayContainer.addEventListener(\n            \"wheel\",\n            (ev) => (this.document.documentElement.scrollTop += ev.deltaY)\n        );\n        /** @type {[BuilderOverlay]} */\n        this.overlays = [];\n        // Refresh the overlays position everytime their target size changes.\n        this.resizeObserver = new ResizeObserver(() => this.refreshPositions());\n\n        this._refreshOverlays = throttleForAnimation(this.refreshOverlays.bind(this));\n\n        // Recompute the overlay when the window is resized.\n        this.addDomListener(window, \"resize\", this._refreshOverlays);\n\n        // On keydown, hide the overlay and then show it again when the mouse\n        // moves.\n        const onMouseMoveOrDown = throttleForAnimation(() => {\n            this.toggleOverlaysVisibility(true);\n            this.refreshPositions();\n            this.editable.removeEventListener(\"mousemove\", onMouseMoveOrDown);\n            this.editable.removeEventListener(\"mousedown\", onMouseMoveOrDown);\n        });\n        this.addDomListener(this.editable, \"keydown\", () => {\n            this.toggleOverlaysVisibility(false);\n            this.editable.addEventListener(\"mousemove\", onMouseMoveOrDown);\n            this.editable.addEventListener(\"mousedown\", onMouseMoveOrDown);\n        });\n\n        // Hide the overlay when scrolling. Show it again when the scroll is\n        // over and recompute its position.\n        const scrollingElement = getScrollingElement(this.document);\n        const scrollingTarget = getScrollingTarget(scrollingElement);\n        this.addDomListener(\n            scrollingTarget,\n            \"scroll\",\n            throttleForAnimation(() => {\n                this.toggleOverlaysVisibility(false);\n                clearTimeout(this.scrollingTimeout);\n                this.scrollingTimeout = setTimeout(() => {\n                    this.toggleOverlaysVisibility(true);\n                    this.refreshPositions();\n                }, 250);\n            }),\n            { capture: true }\n        );\n\n        this._cleanups.push(() => {\n            this.removeBuilderOverlays();\n            this.resizeObserver.disconnect();\n        });\n    }\n\n    openBuilderOverlays(optionsContainer) {\n        this.removeBuilderOverlays();\n        if (!optionsContainer.length) {\n            return;\n        }\n\n        // Create the overlays.\n        optionsContainer.forEach((option) => {\n            const overlay = new BuilderOverlay(option.element, {\n                iframe: this.iframe,\n                overlayContainer: this.overlayContainer,\n                history: this.dependencies.history,\n                hasOverlayOptions: checkElement(option.element, {}) && option.hasOverlayOptions,\n                next: this.dependencies.operation.next,\n                isMobileView: this.config.isMobileView,\n                mobileBreakpoint: this.config.mobileBreakpoint,\n                isRtl: this.config.isEditableRTL,\n            });\n            this.overlays.push(overlay);\n            this.overlayContainer.append(overlay.overlayElement);\n            this.resizeObserver.observe(overlay.overlayTarget, { box: \"border-box\" });\n        });\n\n        // Activate the last overlay.\n        const innermostOverlay = this.overlays.at(-1);\n        innermostOverlay.toggleOverlay(true);\n\n        // Also activate the closest overlay that should have overlay options.\n        if (!innermostOverlay.hasOverlayOptions) {\n            for (let i = this.overlays.length - 2; i >= 0; i--) {\n                const parentOverlay = this.overlays[i];\n                if (parentOverlay.hasOverlayOptions) {\n                    parentOverlay.toggleOverlay(true);\n                    break;\n                }\n            }\n        }\n    }\n\n    removeBuilderOverlays() {\n        this.overlays.forEach((overlay) => {\n            overlay.destroy();\n            overlay.overlayElement.remove();\n            this.resizeObserver.unobserve(overlay.overlayTarget);\n        });\n        this.overlays = [];\n    }\n\n    refreshOverlays() {\n        this.overlays.forEach((overlay) => {\n            overlay.refreshPosition();\n            overlay.refreshHandles();\n        });\n    }\n\n    refreshPositions() {\n        this.overlays.forEach((overlay) => {\n            overlay.refreshPosition();\n        });\n    }\n\n    toggleOverlaysVisibility(show) {\n        this.overlays.forEach((overlay) => {\n            overlay.toggleOverlayVisibility(show);\n        });\n    }\n\n    showOverlayPreview(el) {\n        // Hide all the active overlays.\n        this.toggleOverlaysVisibility(false);\n        // Show the preview of the one corresponding to the given element.\n        const overlayToShow = this.overlays.find((overlay) => overlay.overlayTarget === el);\n        if (!overlayToShow) {\n            return;\n        }\n        overlayToShow.toggleOverlayPreview(true);\n        overlayToShow.toggleOverlayVisibility(true);\n    }\n\n    hideOverlayPreview(el) {\n        // Remove the preview.\n        const overlayToHide = this.overlays.find((overlay) => overlay.overlayTarget === el);\n        if (!overlayToHide) {\n            return;\n        }\n        overlayToHide.toggleOverlayPreview(false);\n        // Show back the active overlays.\n        this.toggleOverlaysVisibility(true);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { basicContainerBuilderComponentProps } from \"../utils\";\nimport { SelectMany2X } from \"./select_many2x\";\n\nexport class BasicMany2Many extends Component {\n    static template = \"html_builder.BasicMany2Many\";\n    static props = {\n        ...basicContainerBuilderComponentProps,\n        model: String,\n        fields: { type: Array, element: String, optional: true },\n        domain: { type: Array, optional: true },\n        limit: { type: Number, optional: true },\n        selection: { type: Array, element: Object },\n        setSelection: Function,\n        create: { type: Function, optional: true },\n    };\n    static components = { SelectMany2X };\n\n    select(entry) {\n        this.props.setSelection([...this.props.selection, entry]);\n    }\n    unselect(id) {\n        this.props.setSelection([...this.props.selection.filter((item) => item.id !== id)]);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport {\n    clickableBuilderComponentProps,\n    useActionInfo,\n    useSelectableItemComponent,\n} from \"../utils\";\nimport { BuilderComponent } from \"./builder_component\";\nimport { Img } from \"../img\";\n\nexport class BuilderButton extends Component {\n    static template = \"html_builder.BuilderButton\";\n    static components = { BuilderComponent, Img };\n    static props = {\n        ...clickableBuilderComponentProps,\n\n        title: { type: String, optional: true },\n        label: { type: String, optional: true },\n        iconImg: { type: String, optional: true },\n        iconImgAlt: { type: String, optional: true },\n        icon: { type: String, optional: true },\n        className: { type: String, optional: true },\n        classActive: { type: String, optional: true },\n        style: { type: String, optional: true },\n        type: { type: String, optional: true },\n\n        slots: { type: Object, optional: true },\n    };\n\n    static defaultProps = {\n        type: \"secondary\",\n    };\n\n    setup() {\n        this.info = useActionInfo();\n        const { state, operation } = useSelectableItemComponent(this.props.id);\n        this.state = state;\n        this.onClick = operation.commit;\n        this.onPointerEnter = operation.preview;\n        this.onPointerLeave = operation.revert;\n    }\n\n    get className() {\n        let className = this.props.className || \"\";\n        if (this.props.type) {\n            className += ` btn-${this.props.type}`;\n        }\n        if (this.state.isActive) {\n            className = `active ${className}`;\n            if (this.props.classActive) {\n                className += ` ${this.props.classActive}`;\n            }\n        }\n        if (this.props.icon) {\n            className += ` o-hb-btn-has-icon`;\n        }\n        if (this.props.iconImg) {\n            className += ` o-hb-btn-has-img-icon`;\n        }\n        return className;\n    }\n\n    get iconClassName() {\n        if (this.props.icon.startsWith(\"fa-\")) {\n            return `fa ${this.props.icon}`;\n        } else if (this.props.icon.startsWith(\"oi-\")) {\n            return `oi ${this.props.icon}`;\n        }\n        return \"\";\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport {\n    basicContainerBuilderComponentProps,\n    useVisibilityObserver,\n    useApplyVisibility,\n    useSelectableComponent,\n} from \"../utils\";\nimport { BuilderComponent } from \"./builder_component\";\n\nexport class BuilderButtonGroup extends Component {\n    static template = \"html_builder.BuilderButtonGroup\";\n    static props = {\n        ...basicContainerBuilderComponentProps,\n        slots: { type: Object, optional: true },\n    };\n    static components = { BuilderComponent };\n\n    setup() {\n        useVisibilityObserver(\"root\", useApplyVisibility(\"root\"));\n\n        useSelectableComponent(this.props.id);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport {\n    clickableBuilderComponentProps,\n    useActionInfo,\n    useClickableBuilderComponent,\n    useDependencyDefinition,\n    useDomState,\n} from \"../utils\";\nimport { BuilderComponent } from \"./builder_component\";\n\nexport class BuilderCheckbox extends Component {\n    static template = \"html_builder.BuilderCheckbox\";\n    static components = { BuilderComponent, CheckBox };\n    static props = {\n        ...clickableBuilderComponentProps,\n    };\n\n    setup() {\n        this.info = useActionInfo();\n        const { operation, isApplied, onReady } = useClickableBuilderComponent();\n        if (this.props.id) {\n            useDependencyDefinition(this.props.id, { isActive: isApplied }, { onReady });\n        }\n        this.state = useDomState(async () => {\n            await onReady;\n            return {\n                isActive: isApplied(),\n            };\n        });\n        this.onPointerEnter = operation.preview;\n        this.onPointerLeave = operation.revert;\n        this.onChange = operation.commit;\n    }\n\n    getClassName() {\n        return \"o-hb-checkbox o_field_boolean o_boolean_toggle form-switch\";\n    }\n}\n", "import { ColorSelector } from \"@html_editor/main/font/color_selector\";\nimport { Component, useComponent, useRef } from \"@odoo/owl\";\nimport {\n    useColorPicker,\n    DEFAULT_COLORS,\n    DEFAULT_THEME_COLOR_VARS,\n} from \"@web/core/color_picker/color_picker\";\nimport { BuilderComponent } from \"./builder_component\";\nimport {\n    basicContainerBuilderComponentProps,\n    getAllActionsAndOperations,\n    revertPreview,\n    useBuilderComponent,\n    useDomState,\n    useHasPreview,\n} from \"../utils\";\nimport { isCSSColor, isColorGradient } from \"@web/core/utils/colors\";\nimport { getAllUsedColors } from \"@html_builder/utils/utils_css\";\n\n// TODO replace by useInputBuilderComponent after extract unit by AGAU\nexport function useColorPickerBuilderComponent() {\n    const comp = useComponent();\n    const { getAllActions, callOperation } = getAllActionsAndOperations(comp);\n    const getAction = comp.env.editor.shared.builderActions.getAction;\n    let selectedTab;\n    const state = useDomState(getState);\n    const applyOperation = comp.env.editor.shared.history.makePreviewableAsyncOperation(\n        (applySpecs, isPreviewing) => {\n            const proms = [];\n            for (const applySpec of applySpecs) {\n                proms.push(\n                    applySpec.action.apply({\n                        isPreviewing,\n                        editingElement: applySpec.editingElement,\n                        params: applySpec.actionParam,\n                        value: applySpec.actionValue,\n                        loadResult: applySpec.loadResult,\n                        dependencyManager: comp.env.dependencyManager,\n                    })\n                );\n            }\n            return Promise.all(proms);\n        }\n    );\n    function getState(editingElement) {\n        // if (!editingElement || !editingElement.isConnected) {\n        //     // TODO try to remove it. We need to move hook in BuilderComponent\n        //     return {};\n        // }\n        const actionWithGetValue = getAllActions().find(\n            ({ actionId }) => getAction(actionId).getValue\n        );\n        const { actionId, actionParam } = actionWithGetValue;\n        const actionValue = getAction(actionId).getValue({ editingElement, params: actionParam });\n        return {\n            // defaultTab is the tab to open if the user has not done a selection yet.\n            // If the user has already selected a color, the tab of the last selection is opened\n            defaultTab: comp.props.selectedTab,\n            mode: actionParam.mainParam || actionId,\n            selectedColor: actionValue || comp.props.defaultColor,\n            selectedColorCombination: comp.env.editor.shared.color.getColorCombination(\n                editingElement,\n                actionParam\n            ),\n            getTargetedElements: () => [editingElement],\n            selectedTab,\n        };\n    }\n    function getColor(colorValue) {\n        return colorValue.startsWith(\"color-prefix-\")\n            ? `var(${colorValue.replace(\"color-prefix-\", \"--\")})`\n            : colorValue;\n    }\n\n    let previewValue = null;\n    function onApply(colorValue) {\n        previewValue = null;\n        selectedTab = comp.getCorrespondingColorPickerTab(colorValue);\n        callOperation(applyOperation.commit, { userInputValue: getColor(colorValue) });\n    }\n    let onPreview = (colorValue) => {\n        // Avoid previewing the same color twice.\n        if (previewValue === colorValue) {\n            return;\n        }\n        previewValue = colorValue;\n        callOperation(applyOperation.preview, {\n            preview: true,\n            userInputValue: getColor(colorValue),\n            operationParams: {\n                cancellable: true,\n                cancelPrevious: () => applyOperation.revert(),\n            },\n        });\n    };\n    const hasPreview = useHasPreview(getAllActions);\n    if (!hasPreview) {\n        onPreview = () => {};\n    }\n    return {\n        state,\n        onApply,\n        onPreview,\n        onPreviewRevert: () => {\n            previewValue = null;\n            revertPreview(comp.env.editor);\n        },\n    };\n}\n\nexport class BuilderColorPicker extends Component {\n    static template = \"html_builder.BuilderColorPicker\";\n    static props = {\n        ...basicContainerBuilderComponentProps,\n        noTransparency: { type: Boolean, optional: true },\n        enabledTabs: { type: Array, optional: true },\n        grayscales: { type: Object, optional: true },\n        unit: { type: String, optional: true },\n        title: { type: String, optional: true },\n        getUsedCustomColors: { type: Function, optional: true },\n        selectedTab: { type: String, optional: true },\n        defaultColor: { type: String, optional: true },\n        defaultOpacity: { type: Number, optional: true },\n    };\n    static defaultProps = {\n        enabledTabs: [\"theme\", \"gradient\", \"custom\"],\n        defaultColor: \"#FFFFFF00\",\n        selectedTab: \"theme\",\n    };\n    static components = {\n        ColorSelector: ColorSelector,\n        BuilderComponent,\n    };\n\n    setup() {\n        useBuilderComponent();\n        const { state, onApply, onPreview, onPreviewRevert } = useColorPickerBuilderComponent();\n        this.colorButton = useRef(\"colorButton\");\n        this.state = state;\n        useColorPicker(\n            \"colorButton\",\n            {\n                state,\n                applyColor: onApply,\n                applyColorPreview: onPreview,\n                applyColorResetPreview: onPreviewRevert,\n                getUsedCustomColors:\n                    this.props.getUsedCustomColors || this.getUsedCustomColors.bind(this),\n                colorPrefix: \"color-prefix-\",\n                cssVarColorPrefix: \"hb-cp-\",\n                noTransparency: this.props.noTransparency,\n                enabledTabs: this.props.enabledTabs,\n                grayscales: this.props.grayscales,\n                defaultOpacity: this.props.defaultOpacity,\n                className: \"o-hb-colorpicker\",\n                editColorCombination: this.env.editColorCombination,\n            },\n            {\n                onClose: onPreviewRevert,\n                popoverClass: \"o-hb-colorpicker-popover\",\n            }\n        );\n    }\n\n    getSelectedColorStyle() {\n        if (this.state.selectedColor) {\n            if (isColorGradient(this.state.selectedColor)) {\n                return `background-image: ${this.state.selectedColor}`;\n            }\n            if (isCSSColor(this.state.selectedColor)) {\n                return `background-color: ${this.state.selectedColor}`;\n            }\n            return `background-color: var(--${this.state.selectedColor})`;\n        }\n        if (this.state.selectedColorCombination) {\n            const colorCombination = this.state.selectedColorCombination.replace(\"_\", \"-\");\n            const el = this.env.getEditingElement();\n            const style = el.ownerDocument.defaultView.getComputedStyle(el);\n            if (style.backgroundImage !== \"none\") {\n                return `background-image: ${style.backgroundImage}`;\n            } else {\n                return `background-color: var(--${colorCombination}-bg)`;\n            }\n        }\n        return \"\";\n    }\n\n    getUsedCustomColors() {\n        return getAllUsedColors(this.env.editor.editable);\n    }\n\n    getCorrespondingColorPickerTab(selectedColor) {\n        if (!selectedColor) {\n            return;\n        }\n\n        selectedColor = selectedColor.replace(/color-prefix-/g, \"\");\n        const isTabEnabled = (tab) => this.props.enabledTabs.includes(tab);\n\n        if (isTabEnabled(\"gradient\") && isColorGradient(selectedColor)) {\n            return \"gradient\";\n        }\n\n        const solidTabColors = [\n            ...DEFAULT_COLORS.flat(),\n            ...DEFAULT_THEME_COLOR_VARS.map((color) => color.toUpperCase()),\n        ];\n        if (isTabEnabled(\"solid\") && solidTabColors.includes(selectedColor.toUpperCase())) {\n            return \"solid\";\n        }\n\n        if (isTabEnabled(\"theme\") && /^o_cc\\d+$/.test(selectedColor)) {\n            return \"theme\";\n        }\n\n        if (isTabEnabled(\"custom\")) {\n            return \"custom\";\n        }\n    }\n}\n", "import { Component, xml } from \"@odoo/owl\";\nimport { useDomState } from \"../utils\";\n\nexport class BuilderComponent extends Component {\n    static template = xml`<t t-if=\"this.state.isVisible\"><t t-slot=\"default\"/></t>`;\n    static props = {\n        slots: { type: Object },\n    };\n\n    setup() {\n        this.state = useDomState(\n            (editingElement) => ({\n                isVisible: !!editingElement,\n            }),\n            { checkEditingElement: false }\n        );\n    }\n}\n", "import { Component, xml } from \"@odoo/owl\";\nimport { basicContainerBuilderComponentProps, useBuilderComponent } from \"../utils\";\nimport { BuilderComponent } from \"./builder_component\";\n\nexport class BuilderContext extends Component {\n    static template = xml`\n        <BuilderComponent>\n            <t t-slot=\"default\"/>\n        </BuilderComponent>\n    `;\n    static props = {\n        ...basicContainerBuilderComponentProps,\n        slots: { type: Object },\n    };\n    static components = {\n        BuilderComponent,\n    };\n\n    setup() {\n        useBuilderComponent();\n    }\n}\n", "import { useDateTimePicker } from \"@web/core/datetime/datetime_picker_hook\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { effect } from \"@web/core/utils/reactive\";\nimport { ConversionError, formatDate, formatDateTime, parseDateTime } from \"@web/core/l10n/dates\";\nimport { pick } from \"@web/core/utils/objects\";\nimport {\n    basicContainerBuilderComponentProps,\n    useBuilderComponent,\n    useInputBuilderComponent,\n} from \"../utils\";\nimport { BuilderComponent } from \"./builder_component\";\nimport { BuilderTextInputBase, textInputBasePassthroughProps } from \"./builder_text_input_base\";\n\nconst { DateTime } = luxon;\n\nexport class BuilderDateTimePicker extends Component {\n    static template = \"html_builder.BuilderDateTimePicker\";\n    static props = {\n        ...basicContainerBuilderComponentProps,\n        ...textInputBasePassthroughProps,\n        type: { type: [{ value: \"date\" }, { value: \"datetime\" }], optional: true },\n        format: { type: String, optional: true },\n        acceptEmptyDate: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        type: \"datetime\",\n        acceptEmptyDate: true,\n    };\n    static components = {\n        BuilderComponent,\n        BuilderTextInputBase,\n    };\n\n    setup() {\n        useBuilderComponent();\n        this.defaultValue = DateTime.now().toUnixInteger().toString();\n        const { state, commit, preview } = useInputBuilderComponent({\n            id: this.props.id,\n            defaultValue: this.props.acceptEmptyDate ? undefined : this.defaultValue,\n            formatRawValue: this.formatRawValue.bind(this),\n            parseDisplayValue: this.parseDisplayValue.bind(this),\n        });\n        this.domState = state;\n        this.state = useState({});\n        effect(\n            ({ value }) => {\n                // State to display in the input.\n                this.state.value = value;\n            },\n            [state]\n        );\n\n        this.commit = (userInputValue) => {\n            this.isPreviewing = false;\n            const result = commit(userInputValue);\n            return result;\n        };\n\n        this.preview = (userInputValue) => {\n            this.isPreviewing = true;\n            preview(userInputValue);\n        };\n\n        const minDate = DateTime.fromObject({ year: 1000 });\n        const maxDate = DateTime.now().plus({ year: 200 });\n        const getPickerProps = () => ({\n            type: this.props.type,\n            minDate,\n            maxDate,\n            value: this.getCurrentValueDateTime(),\n            rounding: 0,\n        });\n\n        this.formatDateTime = this.props.type === \"date\" ? formatDate : formatDateTime;\n\n        this.dateTimePicker = useDateTimePicker({\n            target: \"root\",\n            format: this.props.format,\n            get pickerProps() {\n                return getPickerProps();\n            },\n            onApply: (value) => {\n                this.commit(this.formatDateTime(value));\n            },\n            onChange: (value) => {\n                const dateString = this.formatDateTime(value);\n                this.preview(dateString);\n                this.state.value = this.parseDisplayValue(dateString);\n            },\n        });\n    }\n\n    /**\n     * @returns {DateTime} the current value of the datetime picker\n     */\n    getCurrentValueDateTime() {\n        return this.domState.value ? DateTime.fromSeconds(parseInt(this.domState.value)) : false;\n    }\n\n    /**\n     * @param {String} rawValue - the raw value in seconds\n     * @returns {String} a formatted date string\n     */\n    formatRawValue(rawValue) {\n        return rawValue ? this.formatDateTime(DateTime.fromSeconds(parseInt(rawValue))) : \"\";\n    }\n\n    /**\n     * @param {String} displayValue - representing a date\n     * @returns {String} number of seconds\n     */\n    parseDisplayValue(displayValue) {\n        if (displayValue === \"\" && this.props.acceptEmptyDate) {\n            return undefined;\n        }\n        try {\n            const parsedDateTime = parseDateTime(displayValue);\n            if (parsedDateTime) {\n                return parsedDateTime.toUnixInteger().toString();\n            }\n        } catch (e) {\n            // A ConversionError means displayValue is an invalid date: fall\n            // back to default value.\n            if (!(e instanceof ConversionError)) {\n                throw e;\n            }\n            if (!this.isPreviewing && displayValue !== \"\") {\n                return this.domState.value;\n            }\n        }\n        return this.defaultValue;\n    }\n\n    /**\n     * @returns {String} a formatted date string\n     */\n    get displayValue() {\n        return this.state.value !== undefined ? this.formatRawValue(this.state.value) : undefined;\n    }\n\n    get textInputBaseProps() {\n        return pick(this.props, ...Object.keys(textInputBasePassthroughProps));\n    }\n\n    onFocus() {\n        this.dateTimePicker.open();\n    }\n}\n", "import { Component, onWillStart } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport {\n    basicContainerBuilderComponentProps,\n    useVisibilityObserver,\n    useApplyVisibility,\n} from \"@html_builder/core/utils\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { BuilderSelect } from \"@html_builder/core/building_blocks/builder_select\";\nimport { BuilderSelectItem } from \"@html_builder/core/building_blocks/builder_select_item\";\n\nexport class BuilderFontFamilyPicker extends Component {\n    static template = \"html_builder.BuilderFontFamilyPicker\";\n    static props = {\n        ...basicContainerBuilderComponentProps,\n        valueParamName: String,\n    };\n    static components = {\n        BuilderSelect,\n        BuilderSelectItem,\n    };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        useVisibilityObserver(\"content\", useApplyVisibility(\"root\"));\n        this.fonts = [];\n        onWillStart(async () => {\n            const fontsData = await this.env.editor.shared.builderFont.getFontsData();\n            this.fonts = fontsData._fonts;\n        });\n    }\n    forwardProps(fontValue) {\n        const result = Object.assign({}, this.props, {\n            [this.props.valueParamName]: fontValue.fontFamilyValue,\n        });\n        delete result.selectMethod;\n        delete result.valueParamName;\n        return result;\n    }\n    async onAddFontClick() {\n        await this.env.editor.shared.websiteFont.addFont(this.props.actionParam);\n    }\n    async onDeleteFontClick(font) {\n        const save = await new Promise((resolve) => {\n            this.env.services.dialog.add(ConfirmationDialog, {\n                body: _t(\n                    \"Deleting a font requires a reload of the page. This will save all your changes and reload the page, are you sure you want to proceed?\"\n                ),\n                confirm: () => resolve(true),\n                cancel: () => resolve(false),\n            });\n        });\n        if (!save) {\n            return;\n        }\n        await this.env.editor.shared.websiteFont.deleteFont(font);\n    }\n}\n", "import { BuilderComponent } from \"@html_builder/core/building_blocks/builder_component\";\nimport {\n    basicContainerBuilderComponentProps,\n    useBuilderComponent,\n    useInputBuilderComponent,\n} from \"@html_builder/core/utils\";\nimport { isSmallInteger } from \"@html_builder/utils/utils\";\nimport { Component, onWillUpdateProps, useRef } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\n\nexport class BuilderList extends Component {\n    static template = \"html_builder.BuilderList\";\n    static props = {\n        ...basicContainerBuilderComponentProps,\n        id: { type: String, optional: true },\n        addItemTitle: { type: String, optional: true },\n        itemShape: {\n            type: Object,\n            values: [\n                { value: \"number\" },\n                { value: \"text\" },\n                { value: \"boolean\" },\n                { value: \"exclusive_boolean\" },\n            ],\n            validate: (value) =>\n                // is not empty object and doesn't include reserved fields\n                Object.keys(value).length > 0 && !Object.keys(value).includes(\"_id\"),\n            optional: true,\n        },\n        default: { optional: true },\n        sortable: { optional: true },\n        hiddenProperties: { type: Array, optional: true },\n        records: { type: String, optional: true },\n        defaultNewValue: { type: Object, optional: true },\n        columnWidth: { optional: true },\n        forbidLastItemRemoval: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        addItemTitle: _t(\"Add\"),\n        itemShape: { value: \"text\" },\n        default: { value: _t(\"Item\") },\n        sortable: true,\n        hiddenProperties: [],\n        mode: \"button\",\n        defaultNewValue: {},\n        columnWidth: {},\n        forbidLastItemRemoval: false,\n    };\n    static components = { BuilderComponent, Dropdown };\n\n    setup() {\n        this.validateProps();\n        this.dropdown = useDropdownState();\n        useBuilderComponent();\n        const { state, commit, preview } = useInputBuilderComponent({\n            id: this.props.id,\n            defaultValue: this.parseDisplayValue([]),\n            parseDisplayValue: this.parseDisplayValue,\n            formatRawValue: this.formatRawValue.bind(this),\n        });\n        this.state = state;\n        this.commit = commit;\n        this.preview = preview;\n        this.allRecords = this.formatRawValue(this.props.records);\n\n        onWillUpdateProps((props) => {\n            this.allRecords = this.formatRawValue(props.records);\n        });\n\n        if (this.props.sortable) {\n            useSortable({\n                enable: () => this.props.sortable,\n                ref: useRef(\"table\"),\n                elements: \".o_row_draggable\",\n                handle: \".o_handle_cell\",\n                cursor: \"grabbing\",\n                placeholderClasses: [\"d-table-row\"],\n                onDrop: (params) => {\n                    const { element, previous } = params;\n                    this.reorderItem(element.dataset.id, previous?.dataset.id);\n                },\n            });\n        }\n    }\n\n    validateProps() {\n        // keys match\n        const itemShapeKeys = Object.keys(this.props.itemShape);\n        const defaultKeys = Object.keys(this.props.default);\n        const allKeys = new Set([...itemShapeKeys, ...defaultKeys]);\n        if (allKeys.size !== itemShapeKeys.length) {\n            throw new Error(\"default properties don't match itemShape\");\n        }\n    }\n\n    getAvailableRecords() {\n        const itemIds = new Set(this.formatRawValue(this.state.value).map((i) => i.id));\n        return this.allRecords.filter((record) => !itemIds.has(record.id));\n    }\n\n    parseDisplayValue(displayValue) {\n        return JSON.stringify(displayValue);\n    }\n\n    formatRawValue(rawValue) {\n        const items = rawValue ? JSON.parse(rawValue) : [];\n        let nextAvailableId = items ? this.getNextAvailableItemId(items) : 0;\n        for (const item of items) {\n            if (!(\"_id\" in item)) {\n                item._id = nextAvailableId.toString();\n                nextAvailableId += 1;\n            }\n        }\n        return items;\n    }\n\n    addItem(ev) {\n        const items = this.formatRawValue(this.state.value);\n        if (!ev.currentTarget.dataset.id) {\n            items.push(this.makeDefaultItem());\n        } else {\n            const matchId = (el) => el.id.toString() === ev.currentTarget.dataset.id.toString();\n            const elementToAdd = this.allRecords.find(matchId);\n            if (!items.some(matchId)) {\n                items.push(elementToAdd);\n            }\n            this.dropdown.close();\n        }\n        this.commit(items);\n    }\n\n    deleteItem(itemId) {\n        const items = this.formatRawValue(this.state.value);\n        this.commit(items.filter((item) => item._id !== itemId));\n    }\n\n    reorderItem(itemId, previousId) {\n        let items = this.formatRawValue(this.state.value);\n        const itemToReorder = items.find((item) => item._id === itemId);\n        items = items.filter((item) => item._id !== itemId);\n\n        const previousItem = items.find((item) => item._id === previousId);\n        const previousItems = items.slice(0, items.indexOf(previousItem) + 1);\n\n        const nextItems = items.slice(items.indexOf(previousItem) + 1, items.length);\n\n        const newItems = [...previousItems, itemToReorder, ...nextItems];\n        this.commit(newItems);\n    }\n\n    makeDefaultItem() {\n        return {\n            ...this.props.defaultNewValue,\n            ...this.props.default,\n            _id: this.getNextAvailableItemId().toString(),\n        };\n    }\n\n    getNextAvailableItemId(items) {\n        items = items || this.formatRawValue(this.state?.value);\n        const biggestId = items\n            .map((item) => parseInt(item._id))\n            .reduce((acc, id) => (id > acc ? id : acc), -1);\n        const nextAvailableId = biggestId + 1;\n        return nextAvailableId;\n    }\n\n    onInput(e) {\n        this.handleValueChange(e.target, false);\n    }\n\n    onChange(e) {\n        this.handleValueChange(e.target, true);\n    }\n\n    handleValueChange(targetInputEl, commitToHistory) {\n        const id = targetInputEl.dataset.id;\n        const propertyName = targetInputEl.name;\n        const isCheckbox = targetInputEl.type === \"checkbox\";\n        const value = isCheckbox ? targetInputEl.checked : targetInputEl.value;\n\n        const items = this.formatRawValue(this.state.value);\n        if (value === true && this.props.itemShape[propertyName] === \"exclusive_boolean\") {\n            for (const item of items) {\n                item[propertyName] = false;\n            }\n        }\n        const item = items.find((item) => item._id === id);\n        item[propertyName] = value;\n        if (!isCheckbox) {\n            item.id = isSmallInteger(value) ? parseInt(value) : value;\n        }\n\n        if (commitToHistory) {\n            this.commit(items);\n        } else {\n            this.preview(items);\n        }\n    }\n}\n", "import { Component, onWillStart, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport {\n    basicContainerBuilderComponentProps,\n    getAllActionsAndOperations,\n    useBuilderComponent,\n    useDomState,\n} from \"../utils\";\nimport { BuilderComponent } from \"./builder_component\";\nimport { BasicMany2Many } from \"./basic_many2many\";\n\nexport class BuilderMany2Many extends Component {\n    static template = \"html_builder.BuilderMany2Many\";\n    static props = {\n        ...basicContainerBuilderComponentProps,\n        model: String,\n        m2oField: { type: String, optional: true },\n        fields: { type: Array, element: String, optional: true },\n        domain: { type: Array, optional: true },\n        limit: { type: Number, optional: true },\n    };\n    static defaultProps = BuilderComponent.defaultProps;\n    static components = { BuilderComponent, BasicMany2Many };\n\n    setup() {\n        useBuilderComponent();\n        this.fields = useService(\"field\");\n        const { getAllActions, callOperation } = getAllActionsAndOperations(this);\n        this.callOperation = callOperation;\n        this.applyOperation = this.env.editor.shared.history.makePreviewableAsyncOperation(\n            this.callApply.bind(this)\n        );\n        this.state = useState({\n            searchModel: undefined,\n        });\n        this.domState = useDomState((el) => {\n            const getAction = this.env.editor.shared.builderActions.getAction;\n            const actionWithGetValue = getAllActions().find(\n                ({ actionId }) => getAction(actionId).getValue\n            );\n            const { actionId, actionParam } = actionWithGetValue;\n            const actionValue = getAction(actionId).getValue({\n                editingElement: el,\n                params: actionParam,\n            });\n            return {\n                selection: JSON.parse(actionValue || \"[]\"),\n            };\n        });\n        onWillStart(async () => {\n            await this.handleProps(this.props);\n        });\n        onWillUpdateProps(async (newProps) => {\n            await this.handleProps(newProps);\n        });\n    }\n    async handleProps(props) {\n        if (props.m2oField) {\n            const modelData = await this.fields.loadFields(props.model, {\n                fieldNames: [props.m2oField],\n            });\n            this.state.searchModel = modelData[props.m2oField].relation;\n            if (!this.state.searchModel) {\n                throw new Error(`m2oField ${props.m2oField} is not a relation field`);\n            }\n        } else {\n            this.state.searchModel = props.model;\n        }\n    }\n    callApply(applySpecs) {\n        const proms = [];\n        for (const applySpec of applySpecs) {\n            proms.push(\n                applySpec.action.apply({\n                    editingElement: applySpec.editingElement,\n                    params: applySpec.actionParam,\n                    value: applySpec.actionValue,\n                    loadResult: applySpec.loadResult,\n                    dependencyManager: this.env.dependencyManager,\n                })\n            );\n        }\n        return proms;\n    }\n    setSelection(newSelection) {\n        this.callOperation(this.applyOperation.commit, {\n            userInputValue: JSON.stringify(newSelection),\n        });\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport {\n    basicContainerBuilderComponentProps,\n    getAllActionsAndOperations,\n    revertPreview,\n    useBuilderComponent,\n    useDependencyDefinition,\n    useDomState,\n    useHasPreview,\n} from \"../utils\";\nimport { BuilderComponent } from \"./builder_component\";\nimport { SelectMany2X } from \"./select_many2x\";\nimport { useCachedModel } from \"../cached_model_utils\";\n\nexport class BuilderMany2One extends Component {\n    static template = \"html_builder.BuilderMany2One\";\n    static props = {\n        ...basicContainerBuilderComponentProps,\n        model: String,\n        fields: { type: Array, element: String, optional: true },\n        domain: { type: Array, optional: true },\n        limit: { type: Number, optional: true },\n        id: { type: String, optional: true },\n        allowUnselect: { type: Boolean, optional: true },\n        defaultMessage: { type: String, optional: true },\n        createAction: { type: String, optional: true },\n        nullText: { type: String, optional: true },\n    };\n    static defaultProps = {\n        ...BuilderComponent.defaultProps,\n        allowUnselect: true,\n    };\n    static components = { BuilderComponent, SelectMany2X };\n\n    setup() {\n        useBuilderComponent();\n        const { getAllActions, callOperation } = getAllActionsAndOperations(this);\n        this.cachedModel = useCachedModel();\n        this.callOperation = callOperation;\n        this.hasPreview = useHasPreview(getAllActions);\n        this.applyOperation = this.env.editor.shared.history.makePreviewableAsyncOperation(\n            this.callApply.bind(this)\n        );\n        const getAction = this.env.editor.shared.builderActions.getAction;\n        const actionWithGetValue = getAllActions().find(\n            ({ actionId }) => getAction(actionId).getValue\n        );\n        const { actionId, actionParam } = actionWithGetValue;\n        const getValue = (el) =>\n            getAction(actionId).getValue({ editingElement: el, params: actionParam });\n        this.domState = useDomState(async (el) => {\n            const selectedString = getValue(el);\n            const selected = selectedString && JSON.parse(selectedString);\n            if (selected && !(\"display_name\" in selected && \"name\" in selected)) {\n                let value;\n                if (!selected.id) {\n                    value = {\n                        display_name: this.props.nullText,\n                        name: this.props.nullText,\n                    };\n                } else {\n                    value = (\n                        await this.cachedModel.ormRead(\n                            this.props.model,\n                            [selected.id],\n                            [\"display_name\", \"name\"]\n                        )\n                    )[0];\n                }\n                Object.assign(selected, value);\n            }\n\n            return { selected };\n        });\n        if (this.props.id) {\n            useDependencyDefinition(this.props.id, {\n                getValue: () => getValue(this.env.getEditingElement()),\n            });\n        }\n\n        if (this.props.createAction) {\n            this.createAction = this.env.editor.shared.builderActions.getAction(\n                this.props.createAction\n            );\n            this.createOperation = this.env.editor.shared.history.makePreviewableOperation(\n                this.createAction.apply\n            );\n        }\n    }\n    callApply(applySpecs, isPreviewing) {\n        const proms = [];\n        for (const applySpec of applySpecs) {\n            if (applySpec.actionValue === undefined) {\n                applySpec.action.clean({\n                    isPreviewing,\n                    editingElement: applySpec.editingElement,\n                    params: applySpec.actionParam,\n                    dependencyManager: this.env.dependencyManager,\n                });\n            } else {\n                proms.push(\n                    applySpec.action.apply({\n                        isPreviewing,\n                        editingElement: applySpec.editingElement,\n                        params: applySpec.actionParam,\n                        value: applySpec.actionValue,\n                        loadResult: applySpec.loadResult,\n                        dependencyManager: this.env.dependencyManager,\n                    })\n                );\n            }\n        }\n        return Promise.all(proms);\n    }\n    select(newSelected) {\n        this.callOperation(this.applyOperation.commit, {\n            userInputValue: newSelected && JSON.stringify(newSelected),\n        });\n    }\n    preview(newSelected) {\n        this.callOperation(this.applyOperation.preview, {\n            preview: true,\n            userInputValue: newSelected && JSON.stringify(newSelected),\n            operationParams: {\n                cancellable: true,\n                cancelPrevious: () => this.applyOperation.revert(),\n            },\n        });\n    }\n    revert() {\n        revertPreview(this.env.editor);\n    }\n    create(name) {\n        const args = { editingElement: this.env.getEditingElement(), value: name };\n        this.env.editor.shared.operation.next(() => this.createOperation.commit(args), {\n            load: () =>\n                this.createAction.load?.(args).then((loadResult) => (args.loadResult = loadResult)),\n        });\n    }\n}\n", "import { convertNumericToUnit, getHtmlStyle } from \"@html_editor/utils/formatting\";\nimport { Component } from \"@odoo/owl\";\nimport {\n    basicContainerBuilderComponentProps,\n    useInputBuilderComponent,\n    useBuilderComponent,\n    useInputDebouncedCommit,\n} from \"../utils\";\nimport { BuilderComponent } from \"./builder_component\";\nimport {\n    BuilderTextInputBase,\n    textInputBasePassthroughProps,\n} from \"@html_builder/core/building_blocks/builder_text_input_base\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\nimport { pick } from \"@web/core/utils/objects\";\n\nexport class BuilderNumberInput extends Component {\n    static template = \"html_builder.BuilderNumberInput\";\n    static props = {\n        ...basicContainerBuilderComponentProps,\n        ...textInputBasePassthroughProps,\n        default: { type: [Number, { value: null }], optional: true },\n        unit: { type: String, optional: true },\n        saveUnit: { type: String, optional: true },\n        step: { type: Number, optional: true },\n        min: { type: Number, optional: true },\n        max: { type: Number, optional: true },\n        composable: { type: Boolean, optional: true },\n        applyWithUnit: { type: Boolean, optional: true },\n    };\n    static components = { BuilderComponent, BuilderTextInputBase };\n    static defaultProps = {\n        composable: false,\n        applyWithUnit: true,\n        default: 0,\n    };\n\n    setup() {\n        if (this.props.saveUnit && !this.props.unit) {\n            throw new Error(\"'unit' must be defined to use the 'saveUnit' props\");\n        }\n\n        useBuilderComponent();\n        const { state, commit, preview } = useInputBuilderComponent({\n            id: this.props.id,\n            defaultValue: this.props.default === null ? null : this.props.default?.toString(),\n            formatRawValue: this.formatRawValue.bind(this),\n            parseDisplayValue: this.parseDisplayValue.bind(this),\n        });\n        this.commit = commit;\n        this.preview = preview;\n        this.state = state;\n\n        this.inputRef = useChildRef();\n        this.debouncedCommitValue = useInputDebouncedCommit(this.inputRef);\n    }\n\n    /**\n     * @param {string | number} values - Values separated by spaces or a number\n     * @param {(string) => string} convertSingleValueFn - Convert a single value\n     */\n    convertSpaceSplitValues(values, convertSingleValueFn) {\n        if (typeof values === \"number\") {\n            return convertSingleValueFn(values.toString());\n        }\n        if (values === null) {\n            return values;\n        }\n        if (!values) {\n            return \"\";\n        }\n        return values.trim().split(/\\s+/g).map(convertSingleValueFn).join(\" \");\n    }\n\n    formatRawValue(rawValue) {\n        return this.convertSpaceSplitValues(rawValue, (value) => {\n            const unit = this.props.unit;\n            const { savedValue, savedUnit } = value.match(\n                /(?<savedValue>[\\d.e+-]+)(?<savedUnit>\\w*)/\n            ).groups;\n            if (savedUnit || this.props.saveUnit) {\n                // Convert value from saveUnit to unit\n                value = convertNumericToUnit(\n                    parseFloat(savedValue),\n                    savedUnit || this.props.saveUnit,\n                    unit,\n                    getHtmlStyle(this.env.getEditingElement().ownerDocument)\n                );\n            }\n            // Put *at most* 3 decimal digits\n            return parseFloat(parseFloat(value).toFixed(3)).toString();\n        });\n    }\n\n    clampValue(value) {\n        if (!value && value !== 0) {\n            return value;\n        }\n        value = parseFloat(value);\n        if (value < this.props.min) {\n            return `${this.props.min}`;\n        }\n        if (value > this.props.max) {\n            return `${this.props.max}`;\n        }\n        return +value.toFixed(3);\n    }\n\n    parseDisplayValue(displayValue) {\n        if (!displayValue) {\n            return displayValue;\n        }\n        displayValue = displayValue.replace(/,/g, \".\");\n        // Only accept 0-9, dot, - sign and space if multiple values are allowed\n        if (this.props.composable) {\n            displayValue = displayValue\n                .trim()\n                .replace(/[^0-9.-\\s]/g, \"\")\n                .replace(/(?<!^|\\s)-/g, \"\");\n        } else {\n            displayValue = displayValue\n                .trim()\n                // Remove any space after a \"-\" to accept \"- 10\" as \"-10\"\n                .replace(/-\\s*/g, \"-\")\n                .split(\" \")[0]\n                .replace(/[^0-9.-]/g, \"\")\n                // Only keep \"-\" if it is at the start\n                .replace(/(?<!^)-/g, \"\")\n                // Only keep the first \".\"\n                .replace(/^([^.]*)\\.?(.*)/, (_, a, b) => a + (b ? \".\" + b.replace(/\\./g, \"\") : \"\"));\n        }\n        displayValue =\n            displayValue.split(\" \").map(this.clampValue.bind(this)).join(\" \") || this.props.default;\n        return this.convertSpaceSplitValues(displayValue, (value) => {\n            if (value === \"\") {\n                return value;\n            }\n            const unit = this.props.unit;\n            const saveUnit = this.props.saveUnit;\n            const applyWithUnit = this.props.applyWithUnit;\n            if (unit && saveUnit) {\n                // Convert value from unit to saveUnit\n                value = convertNumericToUnit(\n                    value,\n                    unit,\n                    saveUnit,\n                    getHtmlStyle(this.env.getEditingElement().ownerDocument)\n                );\n            }\n            if (unit && applyWithUnit) {\n                if (saveUnit || saveUnit === \"\") {\n                    value = value + saveUnit;\n                } else {\n                    value = value + unit;\n                }\n            }\n            return value;\n        });\n    }\n\n    get displayValue() {\n        return this.formatRawValue(this.state.value);\n    }\n\n    onKeydown(e) {\n        if (![\"ArrowUp\", \"ArrowDown\"].includes(e.key)) {\n            return;\n        }\n        const values = e.target.value.split(\" \").map((number) => parseFloat(number) || 0);\n        if (e.key === \"ArrowUp\") {\n            values.forEach((value, i) => {\n                values[i] = this.clampValue(value + (this.props.step || 1));\n            });\n        } else if (e.key === \"ArrowDown\") {\n            values.forEach((value, i) => {\n                values[i] = this.clampValue(value - (this.props.step || 1));\n            });\n        }\n        e.target.value = values.join(\" \");\n        this.preview(e.target.value);\n        this.debouncedCommitValue();\n    }\n\n    get textInputBaseProps() {\n        return pick(this.props, ...Object.keys(textInputBasePassthroughProps));\n    }\n}\n", "import { Component, useRef } from \"@odoo/owl\";\nimport {\n    basicContainerBuilderComponentProps,\n    useActionInfo,\n    useBuilderComponent,\n    useInputBuilderComponent,\n    useInputDebouncedCommit,\n} from \"../utils\";\nimport { BuilderComponent } from \"./builder_component\";\n\nexport class BuilderRange extends Component {\n    static template = \"html_builder.BuilderRange\";\n    static props = {\n        ...basicContainerBuilderComponentProps,\n        min: { type: Number, optional: true },\n        max: { type: Number, optional: true },\n        step: { type: Number, optional: true },\n        displayRangeValue: { type: Boolean, optional: true },\n        computedOutput: { type: Function, optional: true },\n        unit: { type: String, optional: true },\n    };\n    static defaultProps = {\n        ...BuilderComponent.defaultProps,\n        min: 0,\n        max: 100,\n        step: 1,\n        displayRangeValue: false,\n    };\n    static components = { BuilderComponent };\n\n    setup() {\n        this.info = useActionInfo();\n        useBuilderComponent();\n        const { state, commit, preview } = useInputBuilderComponent({\n            id: this.props.id,\n            formatRawValue: this.formatRawValue.bind(this),\n            parseDisplayValue: this.parseDisplayValue.bind(this),\n        });\n\n        this.inputRef = useRef(\"inputRef\");\n        this.debouncedCommitValue = useInputDebouncedCommit(this.inputRef);\n\n        this.commit = commit;\n        this.preview = preview;\n        this.state = state;\n    }\n\n    formatRawValue(value) {\n        if (this.props.unit) {\n            // Remove the unit\n            value = value.slice(0, -this.props.unit.length);\n        }\n        return value;\n    }\n\n    parseDisplayValue(value) {\n        if (this.props.unit) {\n            // Add the unit\n            value = `${value}${this.props.unit}`;\n        }\n        return value;\n    }\n\n    onChange(e) {\n        const normalizedDisplayValue = this.commit(e.target.value);\n        e.target.value = normalizedDisplayValue;\n    }\n\n    onInput(e) {\n        this.preview(e.target.value);\n        if (this.props.displayRangeValue) {\n            this.state.value = this.parseDisplayValue(e.target.value);\n        }\n    }\n\n    onKeydown(e) {\n        if (![\"ArrowLeft\", \"ArrowUp\", \"ArrowDown\", \"ArrowRight\"].includes(e.key)) {\n            return;\n        }\n        e.preventDefault();\n        let value = parseInt(e.target.value);\n        if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") {\n            value = Math.max(this.min, value - this.props.step);\n        } else {\n            value = Math.min(this.max, value + this.props.step);\n        }\n        e.target.value = value;\n        this.onInput(e);\n        this.debouncedCommitValue();\n    }\n\n    get rangeInputValue() {\n        return this.state.value ? this.formatRawValue(this.state.value) : \"0\";\n    }\n\n    get displayValue() {\n        let value = this.rangeInputValue;\n        if (this.props.computedOutput) {\n            value = this.props.computedOutput(value);\n        } else if (this.props.unit) {\n            value = `${value}${this.props.unit}`;\n        }\n        return value;\n    }\n\n    get className() {\n        const baseClasses = \"p-0 border-0\";\n        return this.props.min > this.props.max ? `${baseClasses} o_we_inverted_range` : baseClasses;\n    }\n\n    get min() {\n        return this.props.min > this.props.max ? this.props.max : this.props.min;\n    }\n\n    get max() {\n        return this.props.min > this.props.max ? this.props.min : this.props.max;\n    }\n}\n", "import { Component, onMounted, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { useTransition } from \"@web/core/transition\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport {\n    basicContainerBuilderComponentProps,\n    useApplyVisibility,\n    useBuilderComponent,\n    useVisibilityObserver,\n} from \"../utils\";\nimport { BuilderComponent } from \"./builder_component\";\n\nexport class BuilderRow extends Component {\n    static template = \"html_builder.BuilderRow\";\n    static components = { BuilderComponent };\n    static props = {\n        ...basicContainerBuilderComponentProps,\n        label: { type: String, optional: true },\n        tooltip: { type: String, optional: true },\n        slots: { type: Object, optional: true },\n        level: { type: Number, optional: true },\n        expand: { type: Boolean, optional: true },\n        initialExpandAnim: { type: Boolean, optional: true },\n        extraLabelClass: { type: String, optional: true },\n        observeCollapseContent: { type: Boolean, optional: true },\n        disabled: { type: Boolean, optional: true },\n        fullRowToggler: { type: Boolean, optional: true },\n    };\n    static defaultProps = { expand: false, observeCollapseContent: false, fullRowToggler: false };\n\n    setup() {\n        useBuilderComponent();\n        useVisibilityObserver(\"content\", useApplyVisibility(\"root\"));\n\n        this.state = useState({\n            expanded: this.props.expand,\n        });\n        this.hasTooltip = this.props.tooltip ? true : undefined;\n\n        if (this.props.slots.collapse) {\n            useVisibilityObserver(\"collapse-content\", useApplyVisibility(\"collapse\"));\n\n            this.collapseContentId = uniqueId(\"builder_collapse_content_\");\n        }\n\n        this.labelRef = useRef(\"label\");\n        this.collapseContentRef = useRef(\"collapse-content\");\n        let isMounted = false;\n\n        onMounted(() => {\n            if (this.props.initialExpandAnim) {\n                setTimeout(() => {\n                    this.toggleCollapseContent();\n                }, 150);\n            }\n        });\n\n        this.transition = useTransition({\n            initialVisibility: this.props.expand,\n            leaveDuration: 350,\n            name: \"hb-collapse-content\",\n        });\n\n        useEffect(\n            (stage) => {\n                const isFirstMount = !isMounted;\n                isMounted = true;\n                const contentEl = this.collapseContentRef.el;\n                if (!contentEl) {\n                    return;\n                }\n\n                const setHeightAuto = () => {\n                    contentEl.style.height = \"auto\";\n                };\n\n                // Skip transition on first mount if expand=true.\n                if (isFirstMount && this.props.expand) {\n                    setHeightAuto();\n                    return;\n                }\n\n                switch (stage) {\n                    case \"enter-active\": {\n                        contentEl.style.height = contentEl.scrollHeight + \"px\";\n                        contentEl.addEventListener(\"transitionend\", setHeightAuto, { once: true });\n                        break;\n                    }\n                    case \"leave\": {\n                        // Collapse from current height to 0\n                        contentEl.style.height = contentEl.scrollHeight + \"px\";\n                        void contentEl.offsetHeight; // force reflow\n                        contentEl.style.height = \"0px\";\n                        break;\n                    }\n                }\n            },\n            () => [this.transition.stage]\n        );\n        this.tooltip = useService(\"tooltip\");\n    }\n\n    getLevelClass() {\n        return this.props.level ? `hb-row-sublevel hb-row-sublevel-${this.props.level}` : \"\";\n    }\n\n    onRowContentClick() {\n        if (this.props.fullRowToggler) {\n            this.toggleCollapseContent();\n        }\n    }\n\n    toggleCollapseContent() {\n        this.state.expanded = !this.state.expanded;\n        this.transition.shouldMount = this.state.expanded;\n    }\n\n    get displayCollapseContent() {\n        return this.transition.shouldMount || this.props.observeCollapseContent;\n    }\n\n    get collapseContentClass() {\n        const isNotVisible = this.props.observeCollapseContent && !this.transition.shouldMount;\n        return `${this.transition.className} ${isNotVisible ? \"d-none\" : \"\"}`;\n    }\n\n    openTooltip() {\n        if (this.hasTooltip === undefined) {\n            const labelEl = this.labelRef.el;\n            this.hasTooltip = labelEl && labelEl.clientWidth < labelEl.scrollWidth;\n        }\n        if (this.hasTooltip) {\n            const tooltip = this.props.tooltip || this.props.label;\n            this.removeTooltip = this.tooltip.add(this.labelRef.el, { tooltip });\n        }\n    }\n\n    closeTooltip() {\n        if (this.removeTooltip) {\n            this.removeTooltip();\n        }\n    }\n}\n", "import { Component, onMounted, useRef, useSubEnv, xml } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport {\n    basicContainerBuilderComponentProps,\n    useVisibilityObserver,\n    useApplyVisibility,\n    useSelectableComponent,\n} from \"../utils\";\nimport { BuilderComponent } from \"./builder_component\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { setElementContent } from \"@web/core/utils/html\";\n\nexport class WithIgnoreItem extends Component {\n    static template = xml`<t t-slot=\"default\"/>`;\n    static props = {\n        slots: { type: Object },\n    };\n    setup() {\n        useSubEnv({\n            ignoreBuilderItem: true,\n        });\n    }\n}\n\nexport class BuilderSelect extends Component {\n    static template = \"html_builder.BuilderSelect\";\n    static props = {\n        ...basicContainerBuilderComponentProps,\n        className: { type: String, optional: true },\n        dropdownContainerClass: { type: String, optional: true },\n        disabled: { type: Boolean, optional: true },\n        slots: {\n            type: Object,\n            shape: {\n                default: Object, // Content is not optional\n                fixedButton: { type: Object, optional: true },\n            },\n        },\n        dropdownClass: { type: String, optional: true },\n    };\n    static defaultProps = { dropdownClass: \"o-hb-select-dropdown\" };\n    static components = {\n        Dropdown,\n        BuilderComponent,\n        WithIgnoreItem,\n    };\n\n    setup() {\n        useVisibilityObserver(\"content\", useApplyVisibility(\"root\"));\n\n        this.dropdown = useDropdownState();\n\n        const buttonRef = useRef(\"button\");\n        let currentLabel;\n        const updateCurrentLabel = () => {\n            if (!this.props.slots.fixedButton) {\n                const newHtml = currentLabel || _t(\"None\");\n                if (buttonRef.el && buttonRef.el.innerHTML !== newHtml) {\n                    setElementContent(buttonRef.el, newHtml);\n                }\n            }\n        };\n        useSelectableComponent(this.props.id, {\n            onItemChange(item) {\n                currentLabel = item.getLabel();\n                updateCurrentLabel();\n            },\n        });\n        onMounted(updateCurrentLabel);\n        useSubEnv({\n            onSelectItem: () => {\n                this.dropdown.close();\n            },\n        });\n    }\n}\n", "import { Component, markup, onMounted, useRef } from \"@odoo/owl\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport {\n    clickableBuilderComponentProps,\n    useActionInfo,\n    useSelectableItemComponent,\n} from \"../utils\";\nimport { BuilderComponent } from \"./builder_component\";\n\nexport class BuilderSelectItem extends Component {\n    static template = \"html_builder.BuilderSelectItem\";\n    static props = {\n        ...clickableBuilderComponentProps,\n        title: { type: String, optional: true },\n        label: { type: String, optional: true },\n        className: { type: String, optional: true },\n        slots: { type: Object, optional: true },\n    };\n    static defaultProps = {\n        className: \"\",\n    };\n    static components = { BuilderComponent };\n\n    setup() {\n        if (!this.env.selectableContext) {\n            throw new Error(\"BuilderSelectItem must be used inside a BuilderSelect component.\");\n        }\n        this.info = useActionInfo();\n        const item = useRef(\"item\");\n        let label = \"\";\n        const getLabel = () => {\n            // todo: it's not clear why the item.el?.innerHTML is not set at in\n            // some cases. We fallback on a previously set value to circumvent\n            // the problem, but it should be investigated.\n\n            label = this.props.label || (item.el ? markup(item.el.innerHTML) : \"\") || label || \"\";\n            return label;\n        };\n\n        onMounted(getLabel);\n\n        const { state, operation } = useSelectableItemComponent(this.props.id, {\n            getLabel,\n        });\n        this.state = state;\n        this.operation = operation;\n\n        this.onFocusin = this.operation.preview;\n        this.onFocusout = this.operation.revert;\n    }\n\n    onClick() {\n        this.env.onSelectItem();\n        this.operation.commit();\n        this.removeKeydown?.();\n    }\n    onKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (hotkey === \"escape\") {\n            this.operation.revert();\n            this.removeKeydown?.();\n        }\n    }\n    onPointerEnter() {\n        this.operation.preview();\n        const _onKeydown = this.onKeydown.bind(this);\n        document.addEventListener(\"keydown\", _onKeydown);\n        this.removeKeydown = () => document.removeEventListener(\"keydown\", _onKeydown);\n    }\n    onPointerLeave() {\n        this.operation.revert();\n        this.removeKeydown();\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { BuilderTextInputBase, textInputBasePassthroughProps } from \"./builder_text_input_base\";\nimport {\n    basicContainerBuilderComponentProps,\n    useInputBuilderComponent,\n    useBuilderComponent,\n} from \"../utils\";\nimport { BuilderComponent } from \"./builder_component\";\n\nexport class BuilderTextInput extends Component {\n    static template = \"html_builder.BuilderTextInput\";\n    static props = {\n        ...basicContainerBuilderComponentProps,\n        ...textInputBasePassthroughProps,\n        prefix: { type: String, optional: true },\n        default: { type: String, optional: true },\n    };\n    static components = {\n        BuilderComponent,\n        BuilderTextInputBase,\n    };\n\n    setup() {\n        useBuilderComponent();\n        const { state, commit, preview } = useInputBuilderComponent({\n            id: this.props.id,\n            defaultValue: this.props.default,\n        });\n        this.commit = commit;\n        this.preview = preview;\n        this.state = state;\n    }\n\n    get textInputBaseProps() {\n        return pick(this.props, ...Object.keys(textInputBasePassthroughProps));\n    }\n}\n", "import { Component, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { useForwardRefToParent } from \"@web/core/utils/hooks\";\nimport { useActionInfo } from \"../utils\";\n\n// Props given to the builder input components that are then passed to the\n// BuilderTextInputBase.\nexport const textInputBasePassthroughProps = {\n    action: { type: String, optional: true },\n    placeholder: { type: String, optional: true },\n    title: { type: String, optional: true },\n    style: { type: String, optional: true },\n    tooltip: { type: String, optional: true },\n    classes: { type: String, optional: true },\n    inputClasses: { type: String, optional: true },\n    prefix: { type: String, optional: true },\n};\n\nexport class BuilderTextInputBase extends Component {\n    static template = \"html_builder.BuilderTextInputBase\";\n    static props = {\n        slots: { type: Object, optional: true },\n        inputRef: { type: Function, optional: true },\n        ...textInputBasePassthroughProps,\n        commit: { type: Function },\n        preview: { type: Function },\n        onFocus: { type: Function, optional: true },\n        onKeydown: { type: Function, optional: true },\n        value: { type: [String, { value: null }], optional: true },\n    };\n\n    setup() {\n        this.isEditing = false;\n        this.info = useActionInfo();\n        this.inputRef = useForwardRefToParent(\"inputRef\");\n        this.state = useState({ value: this.props.value });\n        onWillUpdateProps((nextProps) => {\n            if (\"value\" in nextProps) {\n                this.state.value = this.isEditing ? this.inputRef.el.value : nextProps.value;\n            }\n        });\n    }\n\n    onChange(ev) {\n        this.isEditing = false;\n        const normalizedDisplayValue = this.props.commit(ev.target.value);\n        ev.target.value = normalizedDisplayValue;\n    }\n\n    onInput(ev) {\n        this.isEditing = true;\n        this.props.preview(ev.target.value);\n    }\n\n    onFocus(ev) {\n        this.props.onFocus?.(ev);\n    }\n\n    onKeydown(ev) {\n        this.props.onKeydown?.(ev);\n    }\n}\n", "import { BuilderComponent } from \"@html_builder/core/building_blocks/builder_component\";\nimport {\n    BuilderTextInputBase,\n    textInputBasePassthroughProps,\n} from \"@html_builder/core/building_blocks/builder_text_input_base\";\nimport {\n    basicContainerBuilderComponentProps,\n    useBuilderComponent,\n    useInputBuilderComponent,\n} from \"@html_builder/core/utils\";\nimport { Component } from \"@odoo/owl\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\nimport { pick } from \"@web/core/utils/objects\";\n\nexport class BuilderUrlPicker extends Component {\n    static template = \"html_builder.BuilderUrlPicker\";\n    static props = {\n        ...basicContainerBuilderComponentProps,\n        ...textInputBasePassthroughProps,\n        default: { type: String, optional: true },\n    };\n    static components = {\n        BuilderComponent,\n        BuilderTextInputBase,\n    };\n\n    setup() {\n        this.inputRef = useChildRef();\n        useBuilderComponent();\n        const { state, commit, preview } = useInputBuilderComponent({\n            id: this.props.id,\n            defaultValue: this.props.default,\n        });\n        this.commit = commit;\n        this.preview = preview;\n        this.state = state;\n    }\n\n    get textInputBaseProps() {\n        return pick(this.props, ...Object.keys(textInputBasePassthroughProps));\n    }\n\n    openPreviewUrl() {\n        if (this.inputRef.el.value) {\n            window.open(this.inputRef.el.value, \"_blank\");\n        }\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nexport class ColorPickerThemeTab extends Component {\n    static template = \"html_builder.ColorPickerThemeTab\";\n    static props = {\n        onColorClick: Function,\n        onColorPointerOver: Function,\n        onColorPointerOut: Function,\n        onColorPointerLeave: Function,\n        onFocusin: Function,\n        onFocusout: Function,\n        selectedColorCombination: { type: String, optional: true },\n        \"*\": { optional: true },\n    };\n}\n\nregistry.category(\"color_picker_tabs\").add(\n    \"html_builder.theme\",\n    {\n        id: \"theme\",\n        name: _t(\"Theme\"),\n        component: ColorPickerThemeTab,\n    },\n    { sequence: 10 }\n);\n", "import { Component, useState, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useDomState } from \"@html_builder/core/utils\";\nimport { useCachedModel } from \"@html_builder/core/cached_model_utils\";\nimport { BuilderComponent } from \"./builder_component\";\nimport { BasicMany2Many } from \"./basic_many2many\";\n\nexport class ModelMany2Many extends Component {\n    static template = \"html_builder.ModelMany2Many\";\n    static props = {\n        //...basicContainerBuilderComponentProps,\n        baseModel: String,\n        recordId: Number,\n        m2oField: String,\n        fields: { type: Array, element: String, optional: true },\n        domain: { type: Array, optional: true },\n        limit: { type: Number, optional: true },\n        createAction: { type: String, optional: true },\n        id: { type: String, optional: true },\n        // currently always allowDelete\n        applyTo: { type: String, optional: true },\n    };\n    static defaultProps = {\n        fields: [],\n        domain: [],\n        limit: 10,\n    };\n    static components = { BuilderComponent, BasicMany2Many };\n\n    setup() {\n        this.fields = useService(\"field\");\n        this.cachedModel = useCachedModel();\n        this.state = useState({\n            searchModel: undefined,\n        });\n        this.modelEdit = undefined;\n        // This `useDomState` is here to get update from history when undo/redo\n        this.domState = useDomState((el) => {\n            if (!this.modelEdit) {\n                return { selection: [] };\n            }\n            return {\n                selection: this.modelEdit.get(this.props.m2oField),\n            };\n        });\n        onWillStart(async () => {\n            await this.handleProps(this.props);\n        });\n        onWillUpdateProps(async (newProps) => {\n            await this.handleProps(newProps);\n        });\n    }\n    async handleProps(props) {\n        const [record] = await this.cachedModel.ormRead(\n            props.baseModel,\n            [props.recordId],\n            [props.m2oField]\n        );\n        const selectedRecordIds = record[props.m2oField];\n        // TODO: handle no record\n        const modelData = await this.fields.loadFields(props.baseModel, {\n            fieldNames: [props.m2oField],\n        });\n        // TODO: simultaneously fly both RPCs\n        this.state.searchModel = modelData[props.m2oField].relation;\n        this.modelEdit = this.cachedModel.useModelEdit({\n            model: this.props.baseModel,\n            recordId: props.recordId,\n        });\n        if (!this.modelEdit.has(props.m2oField)) {\n            const storedSelection = await this.cachedModel.ormRead(\n                this.state.searchModel,\n                selectedRecordIds,\n                [\"display_name\"]\n            );\n            for (const item of storedSelection) {\n                item.name = item.display_name;\n            }\n            this.modelEdit.init(props.m2oField, [...storedSelection]);\n        }\n        this.domState.selection = this.modelEdit.get(props.m2oField);\n    }\n    setSelection(newSelection) {\n        this.modelEdit.set(this.props.m2oField, newSelection);\n        this.env.editor.shared.history.addStep();\n    }\n    create(name) {\n        // TODO maybe this can be in base layer\n        this.setSelection([\n            ...this.domState.selection,\n            {\n                id: `new-${uniqueId()}`,\n                name: name,\n                display_name: name,\n                model: this.state.searchModel,\n            },\n        ]);\n    }\n}\n", "import { Component, useState, onWillUpdateProps, onWillDestroy } from \"@odoo/owl\";\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\nimport { useCachedModel } from \"@html_builder/core/cached_model_utils\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { useDropdownCloser } from \"@web/core/dropdown/dropdown_hooks\";\nimport { shallowEqual } from \"@web/core/utils/arrays\";\n\nclass SelectMany2XCreate extends Component {\n    static template = \"html_builder.SelectMany2XCreate\";\n    static props = {\n        name: String,\n        create: Function,\n    };\n\n    setup() {\n        this.dropdown = useDropdownCloser();\n        this.create = this.create.bind(this);\n    }\n\n    create() {\n        this.dropdown.close();\n        this.props.create(this.props.name);\n    }\n}\n\nexport class SelectMany2X extends Component {\n    static template = \"html_builder.SelectMany2X\";\n    static props = {\n        model: String,\n        fields: { type: Array, element: String, optional: true },\n        domain: { type: Array, optional: true },\n        limit: { type: Number, optional: true },\n        selected: {\n            type: Array,\n            element: { type: Object, shape: { id: [Number, String], \"*\": true } },\n        },\n        select: Function,\n        preview: { type: Function, optional: true },\n        revert: { type: Function, optional: true },\n        closeOnEnterKey: { type: Boolean, optional: true },\n        message: { type: String, optional: true },\n        create: { type: Function, optional: true },\n        nullText: { type: String, optional: true },\n    };\n    static defaultProps = {\n        fields: [],\n        domain: [],\n        limit: 5,\n        closeOnEnterKey: true,\n        message: _t(\"Choose a record...\"),\n    };\n    static components = { SelectMenu, SelectMany2XCreate };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.cachedModel = useCachedModel();\n        this.prevSelectedIds = undefined;\n        this.prevSearchValue = undefined;\n        this.state = useState({\n            nameToCreate: \"\",\n            searchResults: [],\n            limit: this.props.limit,\n        });\n        onWillUpdateProps(async (newProps) => {\n            if (this.searchInvalidationKey(this.props) !== this.searchInvalidationKey(newProps)) {\n                this.prevSelectedIds = undefined;\n                this.prevSearchValue = undefined;\n                this.state.searchResults = [];\n            }\n        });\n        this.menuRef = useChildRef();\n        onWillDestroy(() => this.removeListeners?.());\n    }\n    onOpened() {\n        const menuEl = this.menuRef.el;\n        if (menuEl) {\n            this.removeListeners?.();\n            const onNavigatedAway = this.onNavigatedAway.bind(this);\n            const onNavigatedBack = this.onNavigatedBack.bind(this);\n            menuEl.addEventListener(\"pointerleave\", onNavigatedAway);\n            menuEl.addEventListener(\"pointerenter\", onNavigatedBack);\n            this.removeListeners = () => {\n                delete this.removeListeners;\n                menuEl.removeEventListener(\"pointerleave\", onNavigatedAway);\n                menuEl.removeEventListener(\"pointerenter\", onNavigatedBack);\n            };\n        }\n    }\n    onClosed() {\n        this.removeListeners?.();\n        this.onNavigatedAway();\n    }\n    searchInvalidationKey(props) {\n        return JSON.stringify([props.model, props.fields, props.domain]);\n    }\n    searchMore(searchValue) {\n        this.state.limit += this.props.limit;\n        this.search(searchValue);\n    }\n    async search(searchValue) {\n        const domain = Object.values(this.props.domain).filter((item) => item !== null);\n        const selectedIds = this.props.selected.map((e) => e.id);\n        if (selectedIds.length) {\n            domain.push([\"id\", \"not in\", selectedIds]);\n        }\n        const tuples = await this.orm.call(this.props.model, \"name_search\", [], {\n            name: searchValue,\n            domain: domain,\n            operator: \"ilike\",\n            limit: this.state.limit + 1,\n        });\n        this.state.hasMore = tuples.length > this.state.limit;\n        const results = await this.cachedModel.ormRead(\n            this.props.model,\n            tuples.map(([id, _name]) => id),\n            [...new Set(this.props.fields).add(\"display_name\").add(\"name\")]\n        );\n        if (this.props.nullText && (!results.length || results[0].id)) {\n            results.unshift({\n                id: 0,\n                name: this.props.nullText,\n                display_name: this.props.nullText,\n            });\n        }\n        this.state.searchResults = results;\n    }\n    filteredSearchResult() {\n        const selectedIds = new Set(this.props.selected.map((e) => e.id));\n        return this.state.searchResults\n            .filter((entry) => !selectedIds.has(entry.id))\n            .slice(0, this.state.limit);\n    }\n    async canCreate(name) {\n        if (!this.props.create || !name.length) {\n            return false;\n        }\n        const allRecords = await this.cachedModel.ormSearchRead(\n            this.props.model,\n            [],\n            [\"id\", \"name\"]\n        );\n        const usedNames = [\n            // Exclude existing names\n            ...allRecords.map((item) => item.name),\n            // Exclude new names\n            ...this.props.selected.map((item) => item.name),\n        ];\n        return !usedNames.includes(name);\n    }\n    async onInput(searchValue) {\n        const selectedIds = this.props.selected.map((e) => e.id);\n        // Avoid redundant search queries when the user toggles the dropdown\n        // without changing the search value or the selected options.\n        if (\n            searchValue === this.prevSearchValue &&\n            shallowEqual(selectedIds, this.prevSelectedIds)\n        ) {\n            return;\n        }\n        this.prevSearchValue = searchValue;\n        this.prevSelectedIds = selectedIds;\n        await this.search(searchValue);\n        this.state.nameToCreate = (await this.canCreate(searchValue)) ? searchValue : \"\";\n    }\n\n    preview(value) {\n        if (this.previewed !== value) {\n            this.previewed = value;\n            this.props.preview?.(value);\n        }\n    }\n    revert() {\n        delete this.previewed;\n        this.props.revert?.();\n    }\n    onNavigated(choice) {\n        choice ? this.preview(choice.value) : this.revert();\n        delete this.lastPreviewed;\n    }\n    onNavigatedAway() {\n        if (\"previewed\" in this) {\n            this.lastPreviewed = this.previewed;\n            this.revert();\n        }\n    }\n    onNavigatedBack() {\n        if (\"lastPreviewed\" in this) {\n            this.preview(this.lastPreviewed);\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { Cache } from \"@web/core/utils/cache\";\nimport { ModelEdit } from \"./cached_model_utils\";\n\n/**\n * @typedef { Object } CachedModelShared\n * @property { CachedModelPlugin['ormRead'] } ormRead\n * @property { CachedModelPlugin['ormSearchRead'] } ormSearchRead\n * @property { CachedModelPlugin['useModelEdit'] } useModelEdit\n */\n\nexport class CachedModelPlugin extends Plugin {\n    static id = \"cachedModel\";\n    static shared = [\"ormRead\", \"ormSearchRead\", \"useModelEdit\"];\n    static dependencies = [\"history\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        save_handlers: this.savePendingRecords.bind(this),\n    };\n    setup() {\n        this.ormReadCache = new Cache(\n            ({ model, ids, fields }) => this.services.orm.read(model, ids, fields),\n            JSON.stringify\n        );\n        this.ormSearchReadCache = new Cache(\n            ({ model, domain, fields }) => this.services.orm.searchRead(model, domain, fields),\n            JSON.stringify\n        );\n        this.modelEditCache = new Cache(\n            ({ model, recordId }) => new ModelEdit(this.dependencies.history, model, recordId),\n            JSON.stringify\n        );\n    }\n    destroy() {\n        this.ormReadCache.invalidate();\n        this.ormSearchReadCache.invalidate();\n        this.modelEditCache.invalidate();\n    }\n    ormRead(model, ids, fields) {\n        const SAFE_NULL = -1;\n        const newIds = ids.map((id) => (id === null ? SAFE_NULL : id));\n        return this.ormReadCache.read({ model, ids: newIds, fields });\n    }\n    ormSearchRead(model, domain, fields) {\n        return this.ormSearchReadCache.read({ model, domain, fields });\n    }\n    useModelEdit({ model, recordId, field }) {\n        const modelEdit = this.modelEditCache.read({ model, recordId, field });\n        // track el ?\n        return modelEdit;\n    }\n    async savePendingRecords() {\n        const inventory = {}; // model => { recordId => { field => value } }\n        for (const modelEdit of Object.values(this.modelEditCache.cache)) {\n            modelEdit.collect(inventory);\n        }\n        // Save inventoried changes.\n        for (const [model, records] of Object.entries(inventory)) {\n            for (const [recordId, record] of Object.entries(records)) {\n                for (const [field, value] of Object.entries(record)) {\n                    // Currently only ids selection values are supported.\n                    const proms = value\n                        .filter((value) => typeof value.id === \"string\")\n                        .map((value) =>\n                            this.services.orm.create(value.model, [{ name: value.name }])\n                        );\n                    const createdIDs = (await Promise.all(proms)).flat();\n                    const ids = value\n                        .filter((value) => typeof value.id === \"number\")\n                        .map((value) => value.id)\n                        .concat(createdIDs);\n                    await this.services.orm.write(model, [parseInt(recordId)], {\n                        [field]: [[6, 0, ids]],\n                    });\n                }\n            }\n        }\n        return !!inventory.length;\n    }\n}\n", "import { useEnv } from \"@odoo/owl\";\n\nexport function useCachedModel() {\n    return useEnv().editor.shared.cachedModel;\n}\n\nexport class ModelEdit {\n    constructor(history, model, recordId) {\n        this.values = {};\n        this.history = history;\n        this.model = model;\n        this.recordId = recordId;\n    }\n    has(field) {\n        return field in this.values;\n    }\n    get(field) {\n        return JSON.parse(this.values[field].current);\n    }\n    init(field, value) {\n        value = JSON.stringify(value);\n        this.values[field] = { initial: value, current: value };\n    }\n    set(field, value) {\n        const previous = this.values[field].current;\n        value = JSON.stringify(value);\n        this.history.applyCustomMutation({\n            apply: () => {\n                this.values[field].current = value;\n            },\n            revert: () => {\n                this.values[field].current = previous;\n            },\n        });\n    }\n    collect(inventory) {\n        const records = inventory[this.model] || {};\n        const record = records[this.recordId] || {};\n        for (const field of Object.keys(this.values)) {\n            if (this.values[field].initial !== this.values[field].current) {\n                inventory[this.model] = records;\n                records[this.recordId] = record;\n                record[field] = JSON.parse(this.values[field].current);\n            }\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { isElementInViewport } from \"@html_builder/utils/utils\";\nimport { isRemovable } from \"./remove_plugin\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\n\n/**\n * @typedef { Object } CloneShared\n * @property { ClonePlugin['cloneElement'] } cloneElement\n */\n\n/**\n * @typedef {((arg: { cloneEl: HTMLElement, originalEl: HTMLElement }) => Promise<void>)[]} on_cloned_handlers\n * Called after an element was cloned and inserted in the DOM.\n *\n * @typedef {((arg: { originalEl: HTMLElement }) => void)[]} on_will_clone_handlers\n * Called on the original element before clone.\n */\n\nconst clonableSelector = \"a.btn:not(.oe_unremovable)\";\n\nexport function isClonable(el) {\n    // TODO and isDraggable\n    return el.matches(clonableSelector) || isRemovable(el);\n}\n\nexport class ClonePlugin extends Plugin {\n    static id = \"clone\";\n    static dependencies = [\"history\", \"builderOptions\", \"dom\"];\n    static shared = [\"cloneElement\"];\n\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_actions: {\n            // Maybe rename cloneItem ?\n            CloneItemAction,\n        },\n        get_overlay_buttons: withSequence(2, {\n            getButtons: this.getActiveOverlayButtons.bind(this),\n        }),\n    };\n\n    setup() {\n        this.overlayTarget = null;\n    }\n\n    getActiveOverlayButtons(target) {\n        if (!isClonable(target)) {\n            this.overlayTarget = null;\n            return [];\n        }\n        const buttons = [];\n        this.overlayTarget = target;\n        const disabledReason = this.dependencies.builderOptions.getCloneDisabledReason(target);\n        buttons.push({\n            class: \"o_snippet_clone fa fa-clone\",\n            title: _t(\"Duplicate\"),\n            disabledReason,\n            handler: async () => {\n                await this.cloneElement(this.overlayTarget, { activateClone: false });\n                this.dependencies.history.addStep();\n            },\n        });\n        return buttons;\n    }\n\n    /**\n     * Duplicates the given element and returns the created clone.\n     *\n     * @param {HTMLElement} el the element to clone\n     * @param {Object}\n     *   - `position`: specifies where to position the clone (first parameter of\n     *     the `insertAdjacentElement` function)\n     *   - `scrollToClone`: true if the we should scroll to the clone (if not in\n     *     the viewport), false otherwise\n     *   - `activateClone`: true if the option containers of the clone should be\n     *     the active ones, false otherwise\n     * @returns {HTMLElement}\n     */\n    async cloneElement(\n        el,\n        { position = \"afterend\", scrollToClone = false, activateClone = true } = {}\n    ) {\n        this.dispatchTo(\"on_will_clone_handlers\", { originalEl: el });\n        const cloneEl = el.cloneNode(true);\n        this.dependencies.dom.removeSystemProperties(cloneEl); // TODO check that\n        el.insertAdjacentElement(position, cloneEl);\n\n        // Update the containers if required.\n        if (activateClone) {\n            this.dependencies.builderOptions.setNextTarget(cloneEl);\n        }\n\n        // Scroll to the clone if required and if it is not visible.\n        if (scrollToClone && !isElementInViewport(cloneEl)) {\n            cloneEl.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n        }\n\n        for (const onCloned of this.getResource(\"on_cloned_handlers\")) {\n            await onCloned({ cloneEl, originalEl: el });\n        }\n\n        return cloneEl;\n    }\n}\n\nexport class CloneItemAction extends BuilderAction {\n    static id = \"addItem\";\n    static dependencies = [\"clone\", \"history\"];\n    async apply({ editingElement, params: { mainParam: itemSelector }, value: position }) {\n        const itemEl = editingElement.querySelector(itemSelector);\n        await this.dependencies.clone.cloneElement(itemEl, { position, scrollToClone: true });\n        this.dependencies.history.addStep();\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { applyNeededCss } from \"@html_builder/utils/utils_css\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\nclass ColorStylePlugin extends Plugin {\n    static id = \"colorStyle\";\n    static dependencies = [\"color\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        apply_color_style_overrides: withSequence(5, (element, cssProp, color) => {\n            applyNeededCss(element, cssProp, color);\n            return true;\n        }),\n        apply_custom_css_style: withSequence(20, this.applyColorStyle.bind(this)),\n    };\n    applyColorStyle({ editingElement, params: { mainParam: styleName = \"\" }, value }) {\n        if (styleName === \"background-color\") {\n            const match = value.match(/var\\(--([a-zA-Z0-9-_]+)\\)/);\n            if (match) {\n                value = `bg-${match[1]}`;\n            }\n            this.dependencies.color.colorElement(editingElement, value, \"backgroundColor\");\n            return true;\n        } else if (styleName === \"color\") {\n            const match = value.match(/var\\(--([a-zA-Z0-9-_]+)\\)/);\n            if (match) {\n                value = `text-${match[1]}`;\n            }\n            this.dependencies.color.colorElement(editingElement, value, \"color\");\n            return true;\n        }\n        return false;\n    }\n}\n\nregistry.category(\"builder-plugins\").add(ColorStylePlugin.id, ColorStylePlugin);\n", "import { ColorUIPlugin as EditorColorUIPlugin } from \"@html_editor/main/font/color_ui_plugin\";\nimport { getAllUsedColors } from \"@html_builder/utils/utils_css\";\n\nexport class ColorUIPlugin extends EditorColorUIPlugin {\n    getUsedCustomColors(mode) {\n        return getAllUsedColors(this.editable);\n    }\n\n    getPropsForColorSelector(type) {\n        const props = { ...super.getPropsForColorSelector(type) };\n        props.cssVarColorPrefix = \"hb-cp-\";\n        return props;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { CSS_SHORTHANDS } from \"@html_builder/utils/utils_css\";\n\n/**\n * Compatibility plugin to handle border-width and border-radius inline style\n * removal (including from inner layers), as the calculation regarding radius of\n * inner layers is now automatically done in CSS via the improved bootstrap\n * rounded-X classes and CSS variables.\n */\n\nconst NEW_STYLES = [\"--box-border-width\", \"--box-border-radius\"];\nconst OLD_STYLES = [\"border-width\", \"border-radius\"];\n\nexport class CompatibilityInlineBorderRemovalPlugin extends Plugin {\n    static id = \"compatibilityInlineBorderRemoval\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        apply_custom_css_style: withSequence(20, this.removeInlineBorderIfNecessary.bind(this)),\n    };\n\n    // The border-width/radius calculation using --box-border-width/radius\n    // variables is done at class level using \"border\" and \"rounded\" classes, so\n    // eventual border-width/radius styles must be removed because they would\n    // otherwise take precedence.\n    removeInlineBorderIfNecessary({ editingElement, params }) {\n        const newStyleBeingEdited = NEW_STYLES.find(\n            (style) =>\n                params.mainParam === style || CSS_SHORTHANDS[style].includes(params.mainParam)\n        );\n        if (newStyleBeingEdited && OLD_STYLES.some((style) => editingElement.style[style])) {\n            // Remove all old inline styles related to border-width/radius as\n            // the new CSS rules + variables rely on both being right, i.e. not\n            // messed up by any inline style...\n            for (const oldStyle of OLD_STYLES) {\n                // TODO even though the part about inner layers below is pure\n                // compatibility, this here should actually be done as the\n                // main feature: editing a CSS variable which controls an unique\n                // property should really enforce removing that property if\n                // already forced as inline style. See CSS_VARIABLE_EDIT_TODO.\n                editingElement.style.setProperty(oldStyle, \"\");\n            }\n            // ... that is why we just always remove inner radius style from\n            // children with %o-we-background-layer classes too (note: the code\n            // that handled adding inline style on child nodes only handled\n            // those specific ones here after).\n            const compatLayerSelectors = [\n                \".o_we_bg_filter\",\n                \".o_bg_video_container\",\n                \".s_parallax_bg\",\n            ];\n            const selector = `:scope > ${compatLayerSelectors.join(\", :scope > \")}`;\n            for (const childNode of editingElement.querySelectorAll(selector)) {\n                childNode.style.setProperty(\"border-radius\", \"\");\n            }\n        }\n        return false;\n    }\n}\n\nregistry\n    .category(\"builder-plugins\")\n    .add(CompatibilityInlineBorderRemovalPlugin.id, CompatibilityInlineBorderRemovalPlugin);\n", "import { convertParamToObject } from \"@html_builder/core/utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\n\nexport class CompositeActionPlugin extends Plugin {\n    static id = \"compositeAction\";\n    static dependencies = [\"builderActions\"];\n\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_actions: {\n            CompositeAction,\n            // Do not use with actions that need a custom reload.\n            // TODO: a class approach to actions would be able to solve that\n            // limitation and would also remove the need to split\n            // `composite` and `reloadComposite`.\n            ReloadCompositeAction,\n        },\n    };\n}\n\nexport class CompositeAction extends BuilderAction {\n    static id = \"composite\";\n    static dependencies = [\"builderActions\"];\n    loadOnClean = true;\n    async prepare({ actionParam: { mainParam: actions }, actionValue }) {\n        const proms = [];\n        for (const actionDef of actions) {\n            const action = this.dependencies.builderActions.getAction(actionDef.action);\n            if (action.has(\"prepare\")) {\n                const actionDescr = { actionId: actionDef.action };\n                if (actionDef.actionParam) {\n                    actionDescr.actionParam = convertParamToObject(actionDef.actionParam);\n                }\n                if (actionDef.actionValue || actionValue) {\n                    actionDescr.actionValue = actionDef.actionValue || actionValue;\n                }\n                proms.push(action.prepare(actionDescr));\n            }\n        }\n        await Promise.all(proms);\n    }\n    getPriority({ params: { mainParam: actions }, value }) {\n        const results = [];\n        for (const actionDef of actions) {\n            const action = this.dependencies.builderActions.getAction(actionDef.action);\n            if (action.has(\"getPriority\")) {\n                const actionDescr = this._getActionDescription({ ...actionDef, value });\n                results.push(action.getPriority(actionDescr));\n            }\n        }\n        // TODO: should this be the max or a sum?\n        return Math.max(...results);\n    }\n    // We arbitrarily keep the result of the 1st action, as we\n    // obviously cannot return more than one value.\n    getValue({ editingElement, params: { mainParam: actions } }) {\n        let actionGetValue;\n        const actionDef = actions.find((actionDef) => {\n            const action = this.dependencies.builderActions.getAction(actionDef.action);\n            if (action.has(\"getValue\")) {\n                actionGetValue = action.getValue;\n            }\n            return !!action.getValue;\n        });\n        if (actionDef) {\n            const actionDescr = this._getActionDescription({\n                editingElement,\n                actionParam: actionDef.actionParam,\n            });\n            return actionGetValue(actionDescr);\n        }\n    }\n    isApplied({ editingElement, params: { mainParam: actions }, value }) {\n        const results = [];\n        for (const actionDef of actions) {\n            const action = this.dependencies.builderActions.getAction(actionDef.action);\n            if (action.has(\"isApplied\")) {\n                const actionDescr = this._getActionDescription({\n                    editingElement,\n                    ...actionDef,\n                    value,\n                });\n                results.push(action.isApplied(actionDescr));\n            }\n        }\n        return !!results.length && results.every((result) => result);\n    }\n    async load({ editingElement, params: { mainParam: actions }, value }) {\n        const loadActions = [];\n        const loadResults = [];\n        for (const actionDef of actions) {\n            const action = this.dependencies.builderActions.getAction(actionDef.action);\n            if (action.has(\"load\")) {\n                const actionDescr = this._getActionDescription({\n                    editingElement,\n                    ...actionDef,\n                    value,\n                });\n                loadActions.push(actionDef.action);\n                // We can't use Promise.all as unrelated loads could have\n                // overriding impacts (like updating/creating the same file)\n                // In such cases, this approach allows to define the order\n                // of actions and ensures predictable load results.\n                loadResults.push(await action.load(actionDescr));\n            }\n        }\n        return loadActions.reduce((acc, actionId, idx) => {\n            acc[actionId] = loadResults[idx];\n            return acc;\n        }, {});\n    }\n    async apply({\n        editingElement,\n        params: { mainParam: actions },\n        value,\n        loadResult,\n        dependencyManager,\n        selectableContext,\n    }) {\n        for (const actionDef of actions) {\n            const action = this.dependencies.builderActions.getAction(actionDef.action);\n            if (action.has(\"apply\")) {\n                const actionDescr = this._getActionDescription({\n                    editingElement,\n                    value,\n                    ...actionDef,\n                    loadResult,\n                    dependencyManager,\n                    selectableContext,\n                });\n                await action.apply(actionDescr);\n            }\n        }\n    }\n    clean({\n        editingElement,\n        params: { mainParam: actions },\n        value,\n        loadResult,\n        dependencyManager,\n        selectableContext,\n        nextAction,\n    }) {\n        for (const actionDef of actions) {\n            const action = this.dependencies.builderActions.getAction(actionDef.action);\n            const actionDescr = this._getActionDescription({\n                editingElement,\n                ...actionDef,\n                value,\n                loadResult,\n                dependencyManager,\n                selectableContext,\n                nextAction,\n            });\n            if (action.has(\"clean\")) {\n                action.clean(actionDescr);\n            } else if (action.has(\"apply\")) {\n                if (loadResult && loadResult[actionDef.action]) {\n                    actionDescr.loadResult = loadResult[actionDef.action];\n                }\n                action.apply(actionDescr);\n            }\n        }\n    }\n    _getActionDescription(action) {\n        const { action: actionId, actionParam, actionValue, value, loadResult } = action;\n        const actionDescr = {};\n        const forwardedSpecs = [\n            \"editingElement\",\n            \"dependencyManager\",\n            \"selectableContext\",\n            \"nextAction\",\n        ];\n        for (const spec of forwardedSpecs) {\n            if (action[spec]) {\n                actionDescr[spec] = action[spec];\n            }\n        }\n        if (actionParam) {\n            actionDescr.params = convertParamToObject(actionParam);\n        }\n        if (actionValue || value) {\n            actionDescr.value = actionValue || value;\n        }\n        if (loadResult && loadResult[actionId]) {\n            actionDescr.loadResult = loadResult[actionId];\n        }\n        return actionDescr;\n    }\n}\n\nexport class ReloadCompositeAction extends CompositeAction {\n    static id = \"reloadComposite\";\n    setup() {\n        this.reload = {};\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { getHtmlStyle } from \"@html_editor/utils/formatting\";\nimport {\n    CSS_SHORTHANDS,\n    applyNeededCss,\n    areCssValuesEqual,\n    normalizeColor,\n} from \"@html_builder/utils/utils_css\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { getValueFromVar } from \"@html_builder/utils/utils\";\n\n/** @typedef {import(\"@html_builder/core/builder_action\").ActionParams} ActionParams */\n/** @typedef {import(\"@html_builder/core/builder_action\").ActionValue} ActionValue */\n/**\n * @typedef {((\n *      editingElement: HTMLElement,\n *      params: ActionParams,\n *      value: ActionValue,\n * ) => boolean)[]} apply_custom_css_style\n */\n\nexport function withoutTransition(editingElement, callback) {\n    if (editingElement.classList.contains(\"o_we_force_no_transition\")) {\n        return callback();\n    }\n    editingElement.classList.add(\"o_we_force_no_transition\");\n    try {\n        return callback();\n    } finally {\n        editingElement.classList.remove(\"o_we_force_no_transition\");\n    }\n}\n\nexport class CoreBuilderActionPlugin extends Plugin {\n    static id = \"coreBuilderAction\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_actions: {\n            ClassAction,\n            AttributeAction,\n            StyleAction,\n            DataAttributeAction,\n            SetClassRangeAction,\n        },\n        system_classes: [\"o_we_force_no_transition\"],\n    };\n}\n\nfunction getStyleValue(el, styleName) {\n    const computedStyle = window.getComputedStyle(el);\n    const cssProps = CSS_SHORTHANDS[styleName] || [styleName];\n    const cssValues = cssProps.map((cssProp) => computedStyle.getPropertyValue(cssProp).trim());\n    if (\n        cssValues.length === 4 &&\n        areCssValuesEqual(cssValues[3], cssValues[1], styleName, computedStyle)\n    ) {\n        cssValues.pop();\n    }\n    if (\n        cssValues.length === 3 &&\n        areCssValuesEqual(cssValues[2], cssValues[0], styleName, computedStyle)\n    ) {\n        cssValues.pop();\n    }\n    if (\n        cssValues.length === 2 &&\n        areCssValuesEqual(cssValues[1], cssValues[0], styleName, computedStyle)\n    ) {\n        cssValues.pop();\n    }\n    return cssValues.join(\" \");\n}\n\nfunction setStyle(el, styleName, value, { extraClass, force = false, allowImportant = true } = {}) {\n    const computedStyle = window.getComputedStyle(el);\n    const cssProps = CSS_SHORTHANDS[styleName] || [styleName];\n    // Always reset the inline style first to not put inline style on an\n    // element which already has this style through css stylesheets.\n    for (const cssProp of cssProps) {\n        el.style.setProperty(cssProp, \"\");\n    }\n    el.classList.remove(extraClass);\n\n    // Replacing ', ' by ',' to prevent attributes with internal space separators from being split:\n    // eg: \"rgba(55, 12, 47, 1.9) 47px\" should be split as [\"rgba(55,12,47,1.9)\", \"47px\"]\n    const values = value.replace(/,\\s/g, \",\").split(/\\s+/g);\n    // Compute missing values:\n    // \"a\" => \"a a a a\"\n    // \"a b\" => \"a b a b\"\n    // \"a b c\" => \"a b c b\"\n    // \"a b c d\" => \"a b c d d d d\"\n    while (values.length < cssProps.length) {\n        const len = values.length;\n        const index = len == 3 ? 1 : len == 1 || len == 2 ? 0 : len - 1;\n        values.push(values[index]);\n    }\n\n    let hasUserValue = false;\n    const applyAllCSS = (values) => {\n        for (let i = cssProps.length - 1; i > 0; i--) {\n            hasUserValue =\n                applyNeededCss(el, cssProps[i], values.pop(), computedStyle, {\n                    force,\n                    allowImportant,\n                }) || hasUserValue;\n        }\n        hasUserValue =\n            applyNeededCss(el, cssProps[0], values.join(\" \"), computedStyle, {\n                force,\n                allowImportant,\n            }) || hasUserValue;\n    };\n    applyAllCSS([...values]);\n\n    if (extraClass) {\n        el.classList.toggle(extraClass, hasUserValue);\n        if (hasUserValue) {\n            // Might have changed because of the class.\n            for (const cssProp of cssProps) {\n                el.style.removeProperty(cssProp);\n            }\n            applyAllCSS(values);\n        }\n    }\n}\n\nexport class ClassAction extends BuilderAction {\n    static id = \"classAction\";\n    getPriority({ params: { mainParam: classNames } = {} }) {\n        return (classNames || \"\")?.trim().split(/\\s+/).filter(Boolean).length || 0;\n    }\n    isApplied({ editingElement, params: { mainParam: classNames } = {} }) {\n        if (classNames === undefined || classNames === \"\") {\n            return true;\n        }\n        return classNames\n            .split(\" \")\n            .every((className) => editingElement.classList.contains(className));\n    }\n    apply({ editingElement, params: { mainParam: classNames } = {} }) {\n        for (const className of (classNames || \"\").split(\" \")) {\n            if (className !== \"\") {\n                editingElement.classList.add(className);\n            }\n        }\n    }\n    clean({ editingElement, params: { mainParam: classNames } = {} }) {\n        for (const className of (classNames || \"\").split(\" \")) {\n            if (className !== \"\") {\n                editingElement.classList.remove(className);\n            }\n        }\n    }\n}\n\nclass AttributeAction extends BuilderAction {\n    static id = \"attributeAction\";\n    getValue({ editingElement, params: { mainParam: attributeName } = {} }) {\n        return editingElement.getAttribute(attributeName);\n    }\n    isApplied({ editingElement, params: { mainParam: attributeName } = {}, value }) {\n        if (value) {\n            return (\n                editingElement.hasAttribute(attributeName) &&\n                editingElement.getAttribute(attributeName) === value\n            );\n        } else {\n            return !editingElement.hasAttribute(attributeName);\n        }\n    }\n    apply({ editingElement, params: { mainParam: attributeName } = {}, value }) {\n        if (value) {\n            editingElement.setAttribute(attributeName, value);\n        } else {\n            editingElement.removeAttribute(attributeName);\n        }\n    }\n    clean({ editingElement, params: { mainParam: attributeName } = {} }) {\n        editingElement.removeAttribute(attributeName);\n    }\n}\n\nexport class DataAttributeAction extends BuilderAction {\n    static id = \"dataAttributeAction\";\n    getValue({ editingElement, params: { mainParam: attributeName } = {} }) {\n        if (!/(^color|Color)($|(?=[A-Z]))/.test(attributeName)) {\n            return editingElement.dataset[attributeName];\n        }\n        const color = normalizeColor(\n            editingElement.dataset[attributeName],\n            getHtmlStyle(this.document)\n        );\n        return color;\n    }\n    isApplied({ editingElement, params: { mainParam: attributeName } = {}, value }) {\n        if (value) {\n            value = getValueFromVar(value.toString());\n            return editingElement.dataset[attributeName] === value;\n        } else {\n            return !(attributeName in editingElement.dataset);\n        }\n    }\n    apply({ editingElement, params: { mainParam: attributeName } = {}, value }) {\n        if (value) {\n            value = getValueFromVar(value.toString());\n            editingElement.dataset[attributeName] = value;\n        } else {\n            delete editingElement.dataset[attributeName];\n        }\n    }\n    clean({ editingElement, params: { mainParam: attributeName } = {} }) {\n        delete editingElement.dataset[attributeName];\n    }\n}\n\n// TODO maybe find a better place for this\nclass SetClassRangeAction extends BuilderAction {\n    static id = \"setClassRange\";\n    getValue({ editingElement, params: { mainParam: classNames } }) {\n        for (const index in classNames) {\n            const className = classNames[index];\n            if (editingElement.classList.contains(className)) {\n                return index;\n            }\n        }\n    }\n    apply({ editingElement, params: { mainParam: classNames }, value: index }) {\n        for (const className of classNames) {\n            if (editingElement.classList.contains(className)) {\n                editingElement.classList.remove(className);\n            }\n        }\n        editingElement.classList.add(classNames[index]);\n    }\n}\n\nexport class StyleAction extends BuilderAction {\n    static id = \"styleAction\";\n    static dependencies = [\"color\"];\n    getValue({ editingElement: el, params: { mainParam: styleName } }) {\n        if (\n            styleName === \"--box-border-width\" ||\n            CSS_SHORTHANDS[\"--box-border-width\"].includes(styleName) ||\n            styleName === \"--box-border-radius\" ||\n            CSS_SHORTHANDS[\"--box-border-radius\"].includes(styleName)\n        ) {\n            // When reading a CSS variable, we need to get the computed value\n            // of the actual property it controls, ideally. Not only because the\n            // panel should reflect what the user actually sees but also because\n            // the user could have forced its own inline style by himself. Also,\n            // by compatibility with how borders were edited in the past.\n            // See CSS_VARIABLE_EDIT_TODO.\n            //\n            // TODO this should probably be more generic. Note that this was\n            // also done as a fix where reading the actual CSS variable value\n            // was simply not working properly because getStyleValue checks the\n            // CSS_SHORTHANDS which obviously do not magically work.\n            styleName = styleName.substring(\"--box-\".length);\n        }\n\n        if (styleName === \"box-shadow\") {\n            const value = getStyleValue(el, styleName);\n            const inset = value.includes(\"inset\");\n            let values = value.replace(/,\\s/g, \",\").replace(\"inset\", \"\").trim().split(/\\s+/g);\n            const color = values.find((s) => !s.match(/^\\d/));\n            values = values.join(\" \").replace(color, \"\").trim();\n            return `${color} ${values}${inset ? \" inset\" : \"\"}`;\n        } else if (\n            styleName === \"border-width\" ||\n            CSS_SHORTHANDS[\"border-width\"].includes(styleName)\n        ) {\n            let value = getStyleValue(el, styleName);\n            if (value.endsWith(\"px\")) {\n                value = value\n                    .split(/\\s+/g)\n                    .map(\n                        (singleValue) =>\n                            // Rounding value up avoids zoom-in issues.\n                            // Zoom-out issues are not an expected use case.\n                            `${Math.ceil(parseFloat(singleValue))}px`\n                    )\n                    .join(\" \");\n            }\n            return value;\n        } else if (styleName === \"row-gap\" || styleName === \"column-gap\") {\n            return parseInt(getStyleValue(el, styleName)) || 0;\n        } else if (styleName === \"width\") {\n            return el.style.width;\n        } else if (styleName === \"background-color\") {\n            return this.dependencies.color.getElementColors(el)[\"backgroundColor\"];\n        } else if (styleName === \"color\") {\n            return this.dependencies.color.getElementColors(el)[\"color\"];\n        }\n        return this._getValueWithoutTransition(el, styleName);\n    }\n    isApplied({ editingElement: el, params: { mainParam: styleName }, value }) {\n        const currentValue = this.getValue({\n            editingElement: el,\n            params: { mainParam: styleName },\n        });\n        return currentValue === value;\n    }\n    apply({ editingElement, params = {}, value }) {\n        if (!this.delegateTo(\"apply_custom_css_style\", { editingElement, params, value })) {\n            this.applyCssStyle({ editingElement, params, value });\n        }\n    }\n    applyCssStyle({ editingElement, params = {}, value }) {\n        params = { ...params };\n        const styleName = params.mainParam;\n        delete params.mainParam;\n        // Disable all transitions for the duration of the method as many\n        // comparisons will be done on the element to know if applying a\n        // property has an effect or not. Also, changing a css property via the\n        // editor should not show any transition as previews would not be done\n        // immediately, which is not good for the user experience.\n        withoutTransition(editingElement, () => {\n            setStyle(editingElement, styleName, value, params);\n        });\n    }\n    _getValueWithoutTransition(el, styleName) {\n        return withoutTransition(el, () => getStyleValue(el, styleName));\n    }\n}\n", "import {\n    MAIN_PLUGINS as MAIN_EDITOR_PLUGINS,\n    NO_EMBEDDED_COMPONENTS_FALLBACK_PLUGINS,\n} from \"@html_editor/plugin_sets\";\nimport { removePlugins } from \"@html_builder/utils/utils\";\nimport { AnchorPlugin } from \"./anchor/anchor_plugin\";\nimport { BuilderActionsPlugin } from \"./builder_actions_plugin\";\nimport { BuilderComponentPlugin } from \"./builder_component_plugin\";\nimport { BuilderOptionsPlugin } from \"./builder_options_plugin\";\nimport { BuilderOverlayPlugin } from \"./builder_overlay/builder_overlay_plugin\";\nimport { CachedModelPlugin } from \"./cached_model_plugin\";\nimport { ClonePlugin } from \"./clone_plugin\";\nimport { ColorUIPlugin } from \"./color_ui_plugin\";\nimport { ImagePlugin } from \"./image_plugin\";\nimport { IconPlugin } from \"./icon_plugin\";\nimport { CoreBuilderActionPlugin } from \"./core_builder_action_plugin\";\nimport { CompositeActionPlugin } from \"./composite_action_plugin\";\nimport { CustomizeTabPlugin } from \"./customize_tab_plugin\";\nimport { DisableSnippetsPlugin } from \"./disable_snippets_plugin\";\nimport { DragAndDropPlugin } from \"./drag_and_drop_plugin\";\nimport { DropZonePlugin } from \"./drop_zone_plugin\";\nimport { DropZoneSelectorPlugin } from \"./dropzone_selector_plugin\";\nimport { GridLayoutPlugin } from \"./grid_layout/grid_layout_plugin\";\nimport { MediaWebsitePlugin } from \"./media_website_plugin\";\nimport { MovePlugin } from \"./move_plugin\";\nimport { OperationPlugin } from \"./operation_plugin\";\nimport { OverlayButtonsPlugin } from \"./overlay_buttons/overlay_buttons_plugin\";\nimport { RemovePlugin } from \"./remove_plugin\";\nimport { SavePlugin } from \"./save_plugin\";\nimport { SaveSnippetPlugin } from \"./save_snippet_plugin\";\nimport { SetupEditorPlugin } from \"./setup_editor_plugin\";\nimport { CoreSetupEditorPlugin } from \"./core_setup_editor_plugin\";\nimport { VisibilityPlugin } from \"./visibility_plugin\";\nimport { FieldChangeReplicationPlugin } from \"./field_change_replication_plugin\";\nimport { BuilderContentEditablePlugin } from \"./builder_content_editable_plugin\";\nimport { ImageFieldPlugin } from \"@html_builder/plugins/image_field_plugin\";\nimport { MonetaryFieldPlugin } from \"@html_builder/plugins/monetary_field_plugin\";\nimport { Many2OneOptionPlugin } from \"@html_builder/plugins/many2one_option_plugin\";\n\nconst mainEditorPluginsToRemove = [\n    \"PowerButtonsPlugin\",\n    \"DoubleClickImagePreviewPlugin\",\n    \"SeparatorPlugin\",\n    \"StarPlugin\",\n    \"BannerPlugin\",\n    \"MoveNodePlugin\",\n    \"FontFamilyPlugin\",\n    // Replaced plugins:\n    \"ColorUIPlugin\",\n    \"ImagePlugin\",\n    \"IconPlugin\",\n];\n\nexport const MAIN_PLUGINS = [\n    ...removePlugins(\n        [...MAIN_EDITOR_PLUGINS, ...NO_EMBEDDED_COMPONENTS_FALLBACK_PLUGINS],\n        mainEditorPluginsToRemove\n    ),\n    ColorUIPlugin,\n    ImagePlugin,\n    IconPlugin,\n];\n\nexport const CORE_PLUGINS = [\n    ...MAIN_PLUGINS,\n    BuilderOptionsPlugin,\n    BuilderActionsPlugin,\n    BuilderComponentPlugin,\n    OperationPlugin,\n    BuilderOverlayPlugin,\n    OverlayButtonsPlugin,\n    MovePlugin,\n    GridLayoutPlugin,\n    DragAndDropPlugin,\n    RemovePlugin,\n    ClonePlugin,\n    SaveSnippetPlugin,\n    AnchorPlugin,\n    DropZonePlugin,\n    DisableSnippetsPlugin,\n    MediaWebsitePlugin,\n    SetupEditorPlugin,\n    CoreSetupEditorPlugin,\n    SavePlugin,\n    VisibilityPlugin,\n    DropZoneSelectorPlugin,\n    CachedModelPlugin,\n    CoreBuilderActionPlugin,\n    CompositeActionPlugin,\n    CustomizeTabPlugin,\n    FieldChangeReplicationPlugin,\n    BuilderContentEditablePlugin,\n    ImageFieldPlugin,\n    MonetaryFieldPlugin,\n    Many2OneOptionPlugin,\n];\n", "import { Plugin } from \"@html_editor/plugin\";\n\nexport class CoreSetupEditorPlugin extends Plugin {\n    // TODO: remove in master\n    static id = \"core_setup_editor_plugin\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        o_editable_selectors: \"[data-oe-model]\",\n    };\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { reactive } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\n/**\n * @typedef { Object } CustomizeTabShared\n * @property { CustomizeTabPlugin['getCustomizeComponent'] } getCustomizeComponent\n * @property { CustomizeTabPlugin['openCustomizeComponent'] } openCustomizeComponent\n * @property { CustomizeTabPlugin['closeCustomizeComponent'] } closeCustomizeComponent\n */\n\nexport class CustomizeTabPlugin extends Plugin {\n    static id = \"customizeTab\";\n    static shared = [\"getCustomizeComponent\", \"openCustomizeComponent\", \"closeCustomizeComponent\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        post_redo_handlers: () => this.closeCustomizeComponent(),\n        post_undo_handlers: () => this.closeCustomizeComponent(),\n        change_current_options_containers_listeners: () => this.closeCustomizeComponent(),\n    };\n\n    setup() {\n        this.customizeComponent = reactive({\n            component: null,\n            props: {},\n            editingEls: null,\n        });\n        this.closeCustomizeComponent = this.closeCustomizeComponent.bind(this);\n    }\n    getCustomizeComponent() {\n        return this.customizeComponent;\n    }\n    openCustomizeComponent(component, editingEls, props = {}) {\n        this.customizeComponent.component = component;\n        this.customizeComponent.editingEls = editingEls;\n        this.customizeComponent.props = {\n            ...props,\n            onClose: this.closeCustomizeComponent,\n        };\n    }\n    closeCustomizeComponent() {\n        if (this.customizeComponent) {\n            this.customizeComponent.component = null;\n            this.customizeComponent.editingEls = null;\n            this.customizeComponent.props = {};\n        }\n    }\n}\n\nregistry.category(\"builder-plugins\").add(CustomizeTabPlugin.id, CustomizeTabPlugin);\n", "import { EventBus } from \"@odoo/owl\";\nimport { batched } from \"@web/core/utils/timing\";\n\n/**\n * @typedef { Object } BuilderOptionDependency\n * @property { () => boolean } isActive\n * @property { Function } [getActions]\n * @property { Function } [getValue]\n * @property { Function } [cleanSelectedItem]\n * @property { string } [type]\n */\n\nexport class DependencyManager extends EventBus {\n    constructor() {\n        super();\n        this.dependencies = [];\n        this.dependenciesMap = {};\n        this.count = 0;\n        this.dirty = false;\n        this.triggerDependencyUpdated = batched(() => {\n            this.trigger(\"dependency-updated\");\n        });\n    }\n    update() {\n        this.dependenciesMap = {};\n        for (const [id, value, ignored] of this.dependencies.slice().reverse()) {\n            if (ignored && id in this.dependenciesMap) {\n                continue;\n            }\n            this.dependenciesMap[id] = value;\n        }\n        this.dirty = false;\n    }\n    /**\n     * @param {string} id\n     * @param {BuilderOptionDependency} value\n     * @param {Boolean} ignored - should not add the dependency to the map\n     */\n    add(id, value, ignored = false) {\n        // In case the dependency is added after a dependent try to get it\n        // an event is scheduled to notify the dependent about it.\n        if (!ignored || !(id in this.dependenciesMap)) {\n            this.triggerDependencyUpdated();\n        }\n        this.dependencies.push([id, value, ignored]);\n        this.dirty = true;\n    }\n    /**\n     * @param {string} id\n     * @returns {BuilderOptionDependency}\n     */\n    get(id) {\n        if (this.dirty) {\n            this.update();\n        }\n        return this.dependenciesMap[id];\n    }\n    /**\n     * @param {BuilderOptionDependency} value\n     */\n    removeByValue(value) {\n        this.dependencies = this.dependencies.filter(([, v]) => v !== value);\n        this.dirty = true;\n        this.triggerDependencyUpdated();\n    }\n}\n", "import { omit } from \"@web/core/utils/objects\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\n/**\n * @typedef { Object } DisableSnippetsShared\n * @property { DisableSnippetsPlugin['disableUndroppableSnippets'] } disableUndroppableSnippets\n */\n\nexport class DisableSnippetsPlugin extends Plugin {\n    static id = \"disableSnippets\";\n    static dependencies = [\"setup_editor_plugin\", \"dropzone\", \"dropzone_selector\"];\n    static shared = [\"disableUndroppableSnippets\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        on_removed_handlers: this.disableUndroppableSnippets.bind(this),\n        post_undo_handlers: this.disableUndroppableSnippets.bind(this),\n        post_redo_handlers: this.disableUndroppableSnippets.bind(this),\n        on_mobile_preview_clicked: withSequence(20, this.disableUndroppableSnippets.bind(this)),\n    };\n\n    setup() {\n        this.snippetModel = this.config.snippetModel;\n        this._disableSnippets = this.disableUndroppableSnippets.bind(this);\n\n        // TODO only for website ?\n        // TODO improve to add case when \"+\" menu appears (resize event ?)\n        const editableDropdownEls = this.editable.querySelectorAll(\".dropdown-menu.o_editable\");\n        editableDropdownEls.forEach((dropdownEl) => {\n            const dropdownToggleEl = dropdownEl.parentNode.querySelector(\".dropdown-toggle\");\n            this.addDomListener(dropdownToggleEl, \"shown.bs.dropdown\", this._disableSnippets);\n            this.addDomListener(dropdownToggleEl, \"hidden.bs.dropdown\", this._disableSnippets);\n        });\n\n        const offcanvasEls = this.editable.querySelectorAll(\".offcanvas\");\n        offcanvasEls.forEach((offcanvasEl) => {\n            this.addDomListener(offcanvasEl, \"shown.bs.offcanvas\", this._disableSnippets);\n            this.addDomListener(offcanvasEl, \"hidden.bs.offcanvas\", this._disableSnippets);\n        });\n\n        this.disableUndroppableSnippets();\n    }\n\n    /**\n     * Makes the snippet that cannot be dropped anywhere appear disabled.\n     * TODO: trigger the computation in the situation that needs it.\n     */\n    disableUndroppableSnippets() {\n        const editableAreaEls = this.dependencies.setup_editor_plugin.getEditableAreas();\n        const rootEl = this.dependencies.dropzone.getDropRootElement();\n        const dropAreasBySelector = this.getDropAreas(editableAreaEls, rootEl);\n\n        // A snippet can only be dropped next/inside elements that are editable\n        // and that do not explicitely block them.\n        const checkSanitize = (el, snippetEl) => {\n            let forbidSanitize = false;\n            // Check if the snippet is sanitized/contains such snippets.\n            for (const el of [snippetEl, ...snippetEl.querySelectorAll(\"[data-snippet\")]) {\n                const snippet = this.snippetModel.getOriginalSnippet(el.dataset.snippet);\n                if (snippet && snippet.forbidSanitize) {\n                    forbidSanitize = snippet.forbidSanitize;\n                    if (forbidSanitize === true) {\n                        break;\n                    }\n                }\n            }\n            if (forbidSanitize === \"form\") {\n                return !el.closest('[data-oe-sanitize]:not([data-oe-sanitize=\"allow_form\"])');\n            } else {\n                return forbidSanitize ? !el.closest(\"[data-oe-sanitize]\") : true;\n            }\n        };\n        const canDrop = (snippet) => {\n            const snippetEl = snippet.content;\n            return !!dropAreasBySelector.find(\n                ({ selector, exclude, dropAreaEls }) =>\n                    snippetEl.matches(selector) &&\n                    !snippetEl.matches(exclude) &&\n                    dropAreaEls.some((el) => checkSanitize(el, snippetEl))\n            );\n        };\n\n        // Disable the snippets that cannot be dropped.\n        const snippetGroups = this.snippetModel.snippetsByCategory[\"snippet_groups\"];\n        let areGroupsDisabled = false;\n        if (snippetGroups.length && !canDrop(snippetGroups[0])) {\n            snippetGroups.forEach((snippetGroup) => (snippetGroup.isDisabled = true));\n            areGroupsDisabled = true;\n        }\n\n        const snippets = [];\n        const ignoredCategories = [\"snippet_groups\"];\n        if (areGroupsDisabled) {\n            ignoredCategories.push(...[\"snippet_structure\", \"snippet_custom\"]);\n        }\n        for (const category in omit(this.snippetModel.snippetsByCategory, ...ignoredCategories)) {\n            snippets.push(...this.snippetModel.snippetsByCategory[category]);\n        }\n        snippets.forEach((snippet) => {\n            snippet.isDisabled = !canDrop(snippet);\n        });\n\n        // Disable the groups containing only disabled snippets.\n        if (!areGroupsDisabled) {\n            snippetGroups.forEach((snippetGroup) => {\n                if (snippetGroup.groupName !== \"custom\") {\n                    snippetGroup.isDisabled = !snippets.find(\n                        (snippet) =>\n                            snippet.groupName === snippetGroup.groupName && !snippet.isDisabled\n                    );\n                } else {\n                    const customSnippets = this.snippetModel.snippetsByCategory[\"snippet_custom\"];\n                    snippetGroup.isDisabled = !customSnippets.find(\n                        (snippet) => !snippet.isDisabled\n                    );\n                }\n            });\n        }\n    }\n\n    /**\n     * Stores the selector/exclude that will make dropzones appear inside the\n     * editable elements, as well as the droppable zones (to compute them only\n     * once).\n     *\n     * @param {Array<HTMLElement>} editableAreaEls\n     * @param {HTMLElement} rootEl\n     * @returns {Array<Object>}\n     */\n    getDropAreas(editableAreaEls, rootEl) {\n        const dropAreasBySelector = [];\n        this.getResource(\"dropzone_selector\").forEach((dropzoneSelector) => {\n            const {\n                selector,\n                exclude = false,\n                dropIn,\n                dropNear,\n                excludeNearParent,\n            } = dropzoneSelector;\n\n            const dropAreaEls = [];\n            if (dropNear) {\n                dropAreaEls.push(\n                    ...this.dependencies.dropzone.getSelectorSiblings(editableAreaEls, rootEl, {\n                        selector: dropNear,\n                        excludeNearParent,\n                    })\n                );\n            }\n            if (dropIn) {\n                dropAreaEls.push(\n                    ...this.dependencies.dropzone.getSelectorChildren(editableAreaEls, rootEl, {\n                        selector: dropIn,\n                    })\n                );\n            }\n            if (dropAreaEls.length) {\n                dropAreasBySelector.push({ selector, exclude, dropAreaEls });\n            }\n        });\n        return dropAreasBySelector;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\n\nexport class DisableSnippetsPlugin extends Plugin {\n    static id = \"disableSnippets\";\n    static shared = [\"disableUndroppableSnippets\"];\n\n    disableUndroppableSnippets() {}\n}\n", "import { Component, onMounted } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class DragAndDropMoveHandle extends Component {\n    static template = \"html_builder.DragAndDropMoveHandle\";\n    static props = {\n        onRenderedCallback: { type: Function },\n    };\n\n    setup() {\n        this.title = _t(\"Drag and move\");\n\n        onMounted(() => {\n            this.props.onRenderedCallback();\n        });\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { useDragAndDrop } from \"@html_editor/utils/drag_and_drop\";\nimport { closestScrollableY, getScrollingElement, isScrollableY } from \"@web/core/utils/scrolling\";\nimport { closest, touching } from \"@web/core/utils/ui\";\nimport { clamp } from \"@web/core/utils/numbers\";\nimport { rowSize } from \"@html_builder/utils/grid_layout_utils\";\nimport { isEditable, isVisible } from \"@html_builder/utils/utils\";\nimport { DragAndDropMoveHandle } from \"./drag_and_drop_move_handle\";\n\n/**\n * @typedef {{\n *     columnWidth: number;\n *     columnHeight: number;\n *     columnSpan: number;\n *     currentDropzoneEl: HTMLElement;\n *     currentHeight: number;\n *     draggedEl: HTMLElement;\n *     dropCloneEl: HTMLElement;\n *     hasSamePositionAsStart?: () => boolean;\n *     marginToAdd: string[];\n *     mousePositionYOnElement: number;\n *     mousePositionXOnElement: number;\n *     overFirstDropzone: boolean;\n *     overGrid: boolean;\n *     restoreCallbacks?: ReturnType<on_prepare_drag_handlers[0]>[] | null;\n *     restoreGridItem?: () => void;\n *     rowSpan: number;\n *     snippet: { gridColumnSpan?: number };\n *     snippetEl: HTMLElement | undefined;\n *     startGridArea: string;\n *     startGridEl: HTMLElement;\n *     startMiddle: number;\n *     startNextEl: HTMLElement | undefined;\n *     startParentEl: HTMLElement;\n *     startPreviousEl: HTMLElement | undefined;\n *     startTop: number;\n *     startZindex: string;\n * }} DragState\n */\n/**\n * @typedef {((arg: {\n *      draggedEl: HTMLElement,\n *      dragState: DragState,\n * }) => void)[]} on_element_dragged_handlers\n * @typedef {((arg: {\n *      droppedEl: HTMLElement,\n *      dragState: DragState,\n * }) => Promise<boolean>)[]} on_element_dropped_handlers\n * @typedef {((arg: {\n *      droppedEl: HTMLElement,\n *      dropzoneEl: HTMLElement,\n *      dragState: DragState,\n * }) => void)[]} on_element_dropped_near_handlers\n * @typedef {((arg: {\n *      droppedEl: HTMLElement,\n *      dragState: DragState,\n * }) => void)[]} on_element_dropped_over_handlers\n * @typedef {((arg: {\n *      draggedEl: HTMLElement,\n *      dragState: DragState,\n *      x: number,\n *      y: number,\n * }) => void)[]} on_element_move_handlers\n * @typedef {((arg: {\n *      draggedEl: HTMLElement,\n *      dragState: DragState,\n * }) => void)[]} on_element_out_dropzone_handlers\n * @typedef {((arg: {\n *      draggedEl: HTMLElement,\n *      dragState: DragState,\n * }) => void)[]} on_element_over_dropzone_handlers\n * @typedef {(() => (() => void))[]} on_prepare_drag_handlers\n *\n * @typedef {((el: HTMLElement) => boolean)[]} is_draggable_handlers\n */\n\nexport class DragAndDropPlugin extends Plugin {\n    static id = \"dragAndDrop\";\n    static dependencies = [\"dropzone\", \"history\", \"operation\", \"builderOptions\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        has_overlay_options: { hasOption: (el) => this.isDraggable(el) },\n        get_overlay_buttons: withSequence(1, {\n            getButtons: this.getActiveOverlayButtons.bind(this),\n        }),\n        system_classes: [\"o_draggable\"],\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n    };\n\n    setup() {\n        this.dropzoneSelectors = this.getResource(\"dropzone_selector\");\n        this.overlayTarget = null;\n        this.iframe = this.document.defaultView.frameElement;\n        this.isRtl = this.config.isEditableRTL;\n    }\n\n    destroy() {\n        this.draggableComponent?.destroy();\n        this.draggableComponentImgs?.destroy();\n    }\n\n    cleanForSave({ root }) {\n        [root, ...root.querySelectorAll(\".o_draggable\")].forEach((el) => {\n            el.classList.remove(\"o_draggable\");\n        });\n    }\n\n    isDraggable(el) {\n        const isDraggable =\n            isEditable(el.parentNode) &&\n            !el.matches(\".oe_unmovable\") &&\n            !!this.dropzoneSelectors.find(\n                ({ selector, exclude = false }) => el.matches(selector) && !el.matches(exclude)\n            );\n        if (!isDraggable) {\n            return false;\n        }\n\n        for (const isDraggable of this.getResource(\"is_draggable_handlers\")) {\n            if (!isDraggable(el)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    getActiveOverlayButtons(target) {\n        if (!this.isDraggable(target)) {\n            this.overlayTarget = null;\n            this.draggableComponent?.destroy();\n            this.draggableComponentImgs?.destroy();\n            return [];\n        }\n\n        const buttons = [];\n        this.overlayTarget = target;\n        buttons.push({\n            Component: DragAndDropMoveHandle,\n            props: {\n                onRenderedCallback: () => {\n                    this.draggableComponent?.destroy();\n                    this.draggableComponentImgs?.destroy();\n\n                    this.draggableComponent = this.initDragAndDrop(\n                        \".o_move_handle\",\n                        \".o_overlay_options\",\n                        document.querySelector(\".o_move_handle\")\n                    );\n                    if (!this.overlayTarget.matches(\"section\")) {\n                        this.draggableComponentImgs = this.initDragAndDrop(\n                            \"img\",\n                            \".o_draggable\",\n                            this.overlayTarget,\n                            true\n                        );\n                    }\n                },\n            },\n        });\n        return buttons;\n    }\n\n    /**\n     * Initializes the drag and drop handles.\n     *\n     * @param {String} handleSelector a selector targeting the handle to drag\n     * @param {String} elementsSelector a selector targeting the element that\n     *   will be dragged\n     * @param {HTMLElement} element the element to listen for drag events\n     * @param {Boolean} [fromIframe=false] true if the dragged element is in the\n     *   iframe\n     * @returns {Object}\n     */\n    initDragAndDrop(handleSelector, elementsSelector, element, fromIframe = false) {\n        let dropzoneEls = [];\n        let dragAndDropResolve;\n\n        const iframeWindow =\n            this.document.defaultView !== window ? this.document.defaultView : false;\n\n        const scrollingElement = () => {\n            let scrollingElement =\n                this.dependencies.dropzone.getDropRootElement() ||\n                getScrollingElement(this.document);\n            if (!isScrollableY(scrollingElement)) {\n                scrollingElement = closestScrollableY(this.iframe) ?? scrollingElement;\n            }\n            return scrollingElement;\n        };\n\n        const dragAndDropOptions = {\n            ref: { el: element },\n            iframeWindow,\n            cursor: \"move\",\n            elements: elementsSelector,\n            scrollingElement,\n            handle: handleSelector,\n            allowDisconnected: true, // To be challenged in master\n            enable: () => !!document.querySelector(\".o_move_handle\") || this.dragStarted, // Still needed ?\n            dropzones: () => dropzoneEls,\n            helper: ({ helperOffset }) => {\n                const draggedEl = document.createElement(\"div\");\n                draggedEl.classList.add(\"o_drag_move_helper\");\n                Object.assign(draggedEl.style, {\n                    width: \"24px\",\n                    height: \"24px\",\n                });\n                document.body.append(draggedEl);\n                const iframeRect = this.document.defaultView.frameElement.getBoundingClientRect();\n                helperOffset.x = 12 - (fromIframe ? iframeRect.x : 0);\n                helperOffset.y = 12;\n                return draggedEl;\n            },\n            onDragStart: ({ x, y }) => {\n                const dragAndDropProm = new Promise(\n                    (resolve) => (dragAndDropResolve = () => resolve())\n                );\n                this.dependencies.operation.next(async () => await dragAndDropProm, {\n                    withLoadingEffect: false,\n                });\n                const restoreDragSavePoint = this.dependencies.history.makeSavePoint();\n                this.cancelDragAndDrop = () => {\n                    this.dependencies.dropzone.removeDropzones();\n                    // Undo the changes needed to ease the drag and drop.\n                    this.dragState.restoreCallbacks?.forEach((restore) => restore());\n                    restoreDragSavePoint();\n                    dragAndDropResolve();\n                    this.dependencies.builderOptions.updateContainers(this.overlayTarget);\n                };\n\n                this.dragStarted = true;\n                /** @type {DragState} */\n                this.dragState = {};\n                dropzoneEls = [];\n\n                // Bound the mouse for the case where we drag from an image.\n                // Bound the Y mouse position to not escape the grid too easily.\n                let targetRect = this.overlayTarget.getBoundingClientRect();\n                const gridRowSize = rowSize;\n                const boundedYMousePosition = clamp(\n                    y,\n                    targetRect.top + 12, // helper offset\n                    targetRect.bottom - gridRowSize // height minus one grid row\n                );\n                this.dragState.mousePositionYOnElement = boundedYMousePosition - targetRect.y;\n                this.dragState.mousePositionXOnElement = (x - targetRect.x) * (this.isRtl ? -1 : 1);\n\n                // Stop marking the elements with mutations as dirty and make\n                // some changes on the page to ease the drag and drop.\n                const restoreCallbacks = [];\n                for (const prepareDrag of this.getResource(\"on_prepare_drag_handlers\")) {\n                    const restore = prepareDrag();\n                    restoreCallbacks.unshift(restore);\n                }\n                this.dragState.restoreCallbacks = restoreCallbacks;\n\n                this.dispatchTo(\"on_element_dragged_handlers\", {\n                    draggedEl: this.overlayTarget,\n                    dragState: this.dragState,\n                });\n\n                // Storing the element starting top and middle position.\n                targetRect = this.overlayTarget.getBoundingClientRect();\n                this.dragState.startTop = targetRect.top;\n                this.dragState.startMiddle = targetRect.left + targetRect.width / 2;\n                this.dragState.overFirstDropzone = true;\n\n                // Check if the element is inline.\n                const targetStyle = window.getComputedStyle(this.overlayTarget);\n                const toInsertInline = targetStyle.display.includes(\"inline\");\n\n                // Store the parent and siblings.\n                const parentEl = this.overlayTarget.parentElement;\n                this.dragState.startParentEl = parentEl;\n                this.dragState.startPreviousEl = this.overlayTarget.previousElementSibling;\n                this.dragState.startNextEl = this.overlayTarget.nextElementSibling;\n\n                // Add a clone, to allow to drop where it started.\n                const visibleSiblingEl = [...parentEl.children].find(\n                    (el) => el !== this.overlayTarget && isVisible(el)\n                );\n                if (parentEl.children.length === 1 || !visibleSiblingEl) {\n                    const dropCloneEl = this.overlayTarget.cloneNode();\n                    dropCloneEl.classList.add(\"oe_drop_clone\");\n                    dropCloneEl.style.visibility = \"hidden\";\n                    dropCloneEl.style.height = `${targetRect.height}px`;\n                    dropCloneEl.style.width = `${targetRect.width}px`;\n                    this.overlayTarget.after(dropCloneEl);\n                    this.dragState.dropCloneEl = dropCloneEl;\n                }\n\n                // Get the dropzone selectors.\n                const isColumn = parentEl.classList.contains(\"row\");\n                const withGrids = isColumn || \"filterOnly\";\n                const selectors = this.dependencies.dropzone.getSelectors(\n                    this.overlayTarget,\n                    true,\n                    withGrids\n                );\n\n                // Remove the dragged element and deactivate the options.\n                this.overlayTarget.remove();\n                this.dependencies.builderOptions.deactivateContainers();\n\n                // Add the dropzones.\n                dropzoneEls = this.dependencies.dropzone.activateDropzones(selectors, {\n                    toInsertInline,\n                });\n            },\n            dropzoneOver: ({ dropzone }) => {\n                const dropzoneEl = dropzone.el;\n\n                // Prevent the element to be trapped in an upper dropzone at the\n                // start of the drag.\n                if (this.dragState.overFirstDropzone) {\n                    this.dragState.overFirstDropzone = false;\n                    const { startTop, startMiddle } = this.dragState;\n                    // The element is considered as glued to the dropzone if the\n                    // dropzone is above and if it is touching the initial\n                    // helper position.\n                    const helperRect = {\n                        x: startMiddle - 12,\n                        y: startTop - 24,\n                        width: 24,\n                        height: 24,\n                    };\n                    const dropzoneRect = dropzoneEl.getBoundingClientRect();\n                    const dropzoneBottom = dropzoneRect.bottom;\n                    const isGluedToDropzone =\n                        startTop >= dropzoneBottom && !!touching([dropzoneEl], helperRect).length;\n                    if (isGluedToDropzone) {\n                        return;\n                    }\n                }\n\n                dropzoneEl.classList.add(\"invisible\");\n                dropzoneEl.after(this.overlayTarget);\n                this.dragState.currentDropzoneEl = dropzoneEl;\n\n                this.dispatchTo(\"on_element_over_dropzone_handlers\", {\n                    draggedEl: this.overlayTarget,\n                    dragState: this.dragState,\n                });\n            },\n            onDrag: ({ x, y }) => {\n                if (!this.dragState.currentDropzoneEl) {\n                    return;\n                }\n\n                this.dispatchTo(\"on_element_move_handlers\", {\n                    draggedEl: this.overlayTarget,\n                    dragState: this.dragState,\n                    x,\n                    y,\n                });\n            },\n            dropzoneOut: () => {\n                const dropzoneEl = this.dragState.currentDropzoneEl;\n                if (!dropzoneEl) {\n                    return;\n                }\n\n                this.dispatchTo(\"on_element_out_dropzone_handlers\", {\n                    draggedEl: this.overlayTarget,\n                    dragState: this.dragState,\n                });\n\n                this.overlayTarget.remove();\n                dropzoneEl.classList.remove(\"invisible\");\n                this.dragState.currentDropzoneEl = null;\n            },\n            onDragEnd: async ({ x, y }) => {\n                this.dragStarted = false;\n                let currentDropzoneEl = this.dragState.currentDropzoneEl;\n                const isDroppedOver = !!currentDropzoneEl;\n\n                // If the snippet was dropped outside of a dropzone, find the\n                // dropzone that is the nearest to the dropping point.\n                if (!currentDropzoneEl) {\n                    const closestDropzoneEl = closest(dropzoneEls, { x, y });\n                    if (!closestDropzoneEl) {\n                        this.cancelDragAndDrop();\n                        return;\n                    }\n                    currentDropzoneEl = closestDropzoneEl;\n                }\n\n                if (isDroppedOver) {\n                    this.dispatchTo(\"on_element_dropped_over_handlers\", {\n                        droppedEl: this.overlayTarget,\n                        dragState: this.dragState,\n                    });\n                } else {\n                    currentDropzoneEl.after(this.overlayTarget);\n                    this.dispatchTo(\"on_element_dropped_near_handlers\", {\n                        droppedEl: this.overlayTarget,\n                        dropzoneEl: currentDropzoneEl,\n                        dragState: this.dragState,\n                    });\n                }\n\n                // In order to mark only the concerned elements as dirty, place\n                // the element back where it started. The move will then be\n                // replayed after re-allowing to mark dirty.\n                const { startPreviousEl, startNextEl, startParentEl } = this.dragState;\n                if (startPreviousEl) {\n                    startPreviousEl.after(this.overlayTarget);\n                } else if (startNextEl) {\n                    startNextEl.before(this.overlayTarget);\n                } else {\n                    startParentEl.prepend(this.overlayTarget);\n                }\n\n                // Undo the changes needed to ease the drag and drop and\n                // re-allow to mark dirty.\n                this.dragState.restoreCallbacks.forEach((restore) => restore());\n                this.dragState.restoreCallbacks = null;\n\n                // Replay the move.\n                currentDropzoneEl.after(this.overlayTarget);\n\n                this.dependencies.dropzone.removeDropzones();\n                this.dragState.dropCloneEl?.remove();\n\n                // Process the dropped element.\n                for (const onElementDropped of this.getResource(\"on_element_dropped_handlers\")) {\n                    const cancel = await onElementDropped({\n                        droppedEl: this.overlayTarget,\n                        dragState: this.dragState,\n                    });\n                    // Cancel everything if the resource asked to.\n                    if (cancel) {\n                        this.cancelDragAndDrop();\n                        return;\n                    }\n                }\n\n                // Add a history step only if the element was not dropped where\n                // it was before, otherwise cancel everything.\n                let hasSamePositionAsStart;\n                if (\"hasSamePositionAsStart\" in this.dragState) {\n                    hasSamePositionAsStart = this.dragState.hasSamePositionAsStart();\n                } else {\n                    const previousEl = this.overlayTarget.previousElementSibling;\n                    const nextEl = this.overlayTarget.nextElementSibling;\n                    const parentEl = this.overlayTarget.parentElement;\n                    hasSamePositionAsStart =\n                        startPreviousEl === previousEl &&\n                        startNextEl === nextEl &&\n                        startParentEl === parentEl;\n                }\n                if (!hasSamePositionAsStart) {\n                    this.dependencies.history.addStep();\n                } else {\n                    this.cancelDragAndDrop();\n                    return;\n                }\n\n                dragAndDropResolve();\n                this.dependencies.builderOptions.updateContainers(this.overlayTarget);\n            },\n        };\n\n        return useDragAndDrop(dragAndDropOptions);\n    }\n}\n", "import { isVisible } from \"@html_builder/utils/utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { isElement } from \"@html_editor/utils/dom_info\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/** @typedef {import(\"plugins\").CSSSelector} CSSSelector */\n/**\n * @typedef {{\n *     selector: CSSSelector;\n *     exclude: CSSSelector;\n *     dropIn: CSSSelector;\n *     dropNear: CSSSelector;\n *     excludeNearParent: CSSSelector;\n * }} DropzoneSelector\n *\n * @typedef { Object } DropZoneShared\n * @property { DropZonePlugin['activateDropzones'] } activateDropzones\n * @property { DropZonePlugin['removeDropzones'] } removeDropzones\n * @property { DropZonePlugin['getDropRootElement'] } getDropRootElement\n * @property { DropZonePlugin['getSelectorSiblings'] } getSelectorSiblings\n * @property { DropZonePlugin['getSelectorChildren'] } getSelectorChildren\n * @property { DropZonePlugin['getSelectors'] } getSelectors\n */\n\n/**\n * @typedef {DropzoneSelector[]} dropzone_selector\n * @typedef {((el: HTMLElement) => boolean)[]} filter_for_sibling_dropzone_predicates\n */\n\nexport class DropZonePlugin extends Plugin {\n    static id = \"dropzone\";\n    static dependencies = [\"history\", \"setup_editor_plugin\"];\n    static shared = [\n        \"activateDropzones\",\n        \"removeDropzones\",\n        \"getDropRootElement\",\n        \"getSelectorSiblings\",\n        \"getSelectorChildren\",\n        \"getSelectors\",\n    ];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        savable_mutation_record_predicates: (record) => {\n            if (record.type === \"childList\") {\n                const addedOrRemovedNode = (record.addedTrees[0] || record.removedTrees[0]).node;\n                // Do not record the addition/removal of the dropzones.\n                if (isElement(addedOrRemovedNode) && addedOrRemovedNode.matches(\".oe_drop_zone\")) {\n                    return false;\n                }\n            }\n            return true;\n        },\n        selection_placeholder_container_predicates: (container) => {\n            if (container.classList.contains(\"oe_structure\")) {\n                return false;\n            }\n        },\n    };\n\n    setup() {\n        this.snippetModel = this.config.snippetModel;\n        this.dropzoneSelectors = this.getResource(\"dropzone_selector\");\n        this.iframe = this.document.defaultView.frameElement;\n    }\n\n    /**\n     * Returns the root element in which the elements can be dropped.\n     * (e.g. if a modal or a dropdown is open, the snippets must be dropped only\n     * in this element)\n     *\n     * @returns {HTMLElement|undefined}\n     */\n    getDropRootElement() {\n        const openModalEl = this.editable.querySelector(\".modal.show\");\n        if (openModalEl && isVisible(openModalEl)) {\n            return openModalEl;\n        }\n        const openDropdownEl = this.editable.querySelector(\n            \".o_editable.dropdown-menu.show, .dropdown-menu.show .o_editable.dropdown-menu\"\n        );\n        if (openDropdownEl) {\n            return openDropdownEl;\n        }\n        const openOffcanvasEl = this.editable.querySelector(\".offcanvas.show\");\n        if (openOffcanvasEl) {\n            return openOffcanvasEl.querySelector(\".offcanvas-body\");\n        }\n    }\n\n    /**\n     * Gets the selectors that determine where the given element can be placed.\n     *\n     * @param {HTMLElement} snippetEl the element\n     * @param {Boolean} [checkLockedWithin=false] true if the selectors should\n     *   be filtered based on the `dropLockWithin` selectors\n     * @param {Boolean|String} [withGrids=false]\n     *   - `true` if the elements in grid mode are considered,\n     *   - `\"filterOnly\"` if the grids should only be filtered out,\n     *   - `false`\n     * @returns {Object} [selectorChildren, selectorSiblings]\n     */\n    getSelectors(snippetEl, checkLockedWithin = false, withGrids = false) {\n        let selectorChildren = [];\n        let selectorSiblings = [];\n        const selectorExcludeAncestor = [];\n        const selectorLockedWithin = [];\n\n        const editableAreaEls = this.dependencies.setup_editor_plugin.getEditableAreas();\n        const rootEl = this.getDropRootElement();\n        this.dropzoneSelectors.forEach((dropzoneSelector) => {\n            const {\n                selector,\n                exclude = false,\n                dropIn,\n                dropNear,\n                dropLockWithin,\n                excludeAncestor,\n                excludeNearParent,\n            } = dropzoneSelector;\n            if (snippetEl.matches(selector) && !snippetEl.matches(exclude)) {\n                if (dropNear) {\n                    selectorSiblings.push(\n                        ...this.getSelectorSiblings(editableAreaEls, rootEl, {\n                            selector: dropNear,\n                            excludeNearParent,\n                        })\n                    );\n                }\n                if (dropIn) {\n                    selectorChildren.push(\n                        ...this.getSelectorChildren(editableAreaEls, rootEl, { selector: dropIn })\n                    );\n                }\n                if (dropLockWithin) {\n                    selectorLockedWithin.push(dropLockWithin);\n                }\n                if (excludeAncestor) {\n                    selectorExcludeAncestor.push(excludeAncestor);\n                }\n            }\n        });\n\n        // Remove the dragged element from the selectors.\n        selectorSiblings = selectorSiblings.filter((el) => !snippetEl.contains(el));\n        selectorChildren = selectorChildren.filter((el) => !snippetEl.contains(el));\n\n        // Prevent dropping an element into another one.\n        // (e.g. ToC inside another ToC)\n        const hasForm = snippetEl.matches(\"form\") || !!snippetEl.querySelector(\"form\");\n        if (hasForm && !selectorExcludeAncestor.includes(\"form\")) {\n            selectorExcludeAncestor.push(\"form\");\n        }\n        if (selectorExcludeAncestor.length) {\n            const excludeAncestor = selectorExcludeAncestor.join(\",\");\n            selectorSiblings = selectorSiblings.filter((el) => !el.closest(excludeAncestor));\n            selectorChildren = selectorChildren.filter((el) => !el.closest(excludeAncestor));\n        }\n\n        // Prevent dropping an element outside a given direct or indirect parent\n        // (e.g. form field must remain within its own form)\n        if (checkLockedWithin && selectorLockedWithin.length) {\n            const lockedAncestorsSelector = selectorLockedWithin.join(\",\");\n            const closestLockedAncestorEl = snippetEl.closest(lockedAncestorsSelector);\n            const filterFct = (el) =>\n                el.closest(lockedAncestorsSelector) === closestLockedAncestorEl;\n            selectorSiblings = selectorSiblings.filter(filterFct);\n            selectorChildren = selectorChildren.filter(filterFct);\n        }\n\n        // Prevent dropping sanitized elements in sanitized zones.\n        let forbidSanitize = false;\n        // Check if the element is sanitized or if it contains such elements.\n        for (const el of [snippetEl, ...snippetEl.querySelectorAll(\"[data-snippet\")]) {\n            const snippet = this.snippetModel.getOriginalSnippet(el.dataset.snippet);\n            if (snippet && snippet.forbidSanitize) {\n                forbidSanitize = snippet.forbidSanitize;\n                if (forbidSanitize === true) {\n                    break;\n                }\n            }\n        }\n        const selectorSanitized = new Set();\n        const filterSanitized = (el) => {\n            if (el.closest('[data-oe-sanitize=\"no_block\"]')) {\n                return false;\n            }\n            let sanitizedZoneEl;\n            if (forbidSanitize === \"form\") {\n                sanitizedZoneEl = el.closest(\n                    '[data-oe-sanitize]:not([data-oe-sanitize=\"allow_form\"]):not([data-oe-sanitize=\"no_block\"])'\n                );\n            } else if (forbidSanitize) {\n                sanitizedZoneEl = el.closest(\n                    '[data-oe-sanitize]:not([data-oe-sanitize=\"no_block\"])'\n                );\n            }\n            if (sanitizedZoneEl) {\n                selectorSanitized.add(sanitizedZoneEl);\n                return false;\n            }\n            return true;\n        };\n        selectorSiblings = selectorSiblings.filter((el) => filterSanitized(el));\n        selectorChildren = selectorChildren.filter((el) => filterSanitized(el));\n\n        // Remove the siblings/children that would add a dropzone as a direct\n        // child of a grid and make a dedicated set out of the identified grids.\n        let selectorGrids = new Set();\n        if (withGrids) {\n            const filterGrids = (potentialGridEl) => {\n                if (potentialGridEl.matches(\".o_grid_mode\")) {\n                    selectorGrids.add(potentialGridEl);\n                    return false;\n                }\n                return true;\n            };\n            selectorSiblings = selectorSiblings.filter((el) => filterGrids(el.parentElement));\n            selectorChildren = selectorChildren.filter((el) => filterGrids(el));\n\n            // If specified, only filter out the grids.\n            if (withGrids === \"filterOnly\") {\n                selectorGrids = new Set();\n            }\n        }\n\n        return {\n            selectorSiblings: new Set(selectorSiblings),\n            selectorChildren: new Set(selectorChildren),\n            selectorSanitized,\n            selectorGrids,\n        };\n    }\n\n    /**\n     * Checks the condition for a sibling/children to be valid.\n     *\n     * @param {HTMLElement} el A selectorSibling or selectorChildren element\n     * @param {HTMLElement} rootEl the root element in which we can drop\n     * @returns {Boolean}\n     */\n    checkSelectors(el, rootEl) {\n        if (rootEl && !rootEl.contains(el)) {\n            return false;\n        }\n        // Drop only in visible elements.\n        if (!isVisible(el)) {\n            return false;\n        }\n        // Drop only in open dropdown and offcanvas elements.\n        if (\n            (el.closest(\".dropdown-menu\") && !el.closest(\".dropdown-menu.show\")) ||\n            (el.closest(\".offcanvas\") && !el.closest(\".offcanvas.show\"))\n        ) {\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * Returns all the elements matching the `dropNear` selector, that are\n     * contained in editable elements. They correspond to elements next to which\n     * an element can be dropped (= siblings).\n     *\n     * @param {Array<HTMLElement>} editableAreaEls the editable elements\n     * @param {HTMLElement} rootEl the root element in which we can drop\n     * @param {String} selector `dropNear` selector\n     * @param {String} excludeParent selector allowing to exclude the siblings\n     * with a parent matching it.\n     * @returns {Array<HTMLElement>}\n     */\n    getSelectorSiblings(editableAreaEls, rootEl, { selector, excludeParent = false }) {\n        const filterFct = (el) =>\n            this.checkSelectors(el, rootEl) &&\n            // Do not drop blocks into an image field.\n            !el.parentNode.closest(\"[data-oe-type=image]\") &&\n            !el.matches(\".o_not_editable *\") &&\n            !el.matches(\".o_we_no_overlay\") &&\n            !this.delegateTo(\"filter_for_sibling_dropzone_predicates\", el) &&\n            (excludeParent ? !el.parentNode.matches(excludeParent) : true);\n\n        const dropAreaEls = [];\n        editableAreaEls.forEach((el) => {\n            const areaEls = [...el.querySelectorAll(selector)].filter(filterFct);\n            dropAreaEls.push(...areaEls);\n        });\n        return dropAreaEls;\n    }\n\n    /**\n     * Returns all the elements matching the `dropIn` selector, that are\n     * contained in editable elements. They correspond to the elements in which\n     * elements can be dropped as children.\n     *\n     * @param {Array<HTMLElement>} editableAreaEls the editable elements\n     * @param {HTMLElement} rootEl the root element in which we can drop\n     * @param {String} selector `dropIn` selector\n     * @returns {Array<HTMLElement>}\n     */\n    getSelectorChildren(editableAreaEls, rootEl, { selector }) {\n        const filterFct = (el) =>\n            this.checkSelectors(el, rootEl) &&\n            // Do not drop blocks into an image field.\n            !el.closest(\"[data-oe-type=image]\") &&\n            !el.matches('.o_not_editable :not([contenteditable=\"true\"]), .o_not_editable');\n\n        const dropAreaEls = [];\n        editableAreaEls.forEach((el) => {\n            const areaEls = el.matches(selector) ? [el] : [];\n            areaEls.push(...el.querySelectorAll(selector));\n            dropAreaEls.push(...areaEls.filter(filterFct));\n        });\n        return dropAreaEls;\n    }\n\n    /**\n     * Creates a dropzone and adapts it depending on the hook environment.\n     *\n     * @param {HTMLElement} parentEl the dropzone parent\n     * @param {Boolean} isVertical true if the dropzone should be vertical\n     * @param {Object} style the style to assign to the dropzone\n     * @returns {HTMLElement}\n     */\n    createDropzone(parentEl, isVertical, style) {\n        const dropzoneEl = this.document.createElement(\"div\");\n        dropzoneEl.classList.add(\"oe_drop_zone\", \"oe_insert\");\n\n        // Set the messages to display in the dropzone.\n        const editorMessagesAttributes = [\n            \"data-editor-message-default\",\n            \"data-editor-message\",\n            \"data-editor-sub-message\",\n        ];\n        for (const messageAttribute of editorMessagesAttributes) {\n            const message = parentEl.getAttribute(messageAttribute);\n            if (message) {\n                dropzoneEl.setAttribute(messageAttribute, message);\n            }\n        }\n\n        if (isVertical) {\n            dropzoneEl.classList.add(\"oe_vertical\");\n        }\n        Object.assign(dropzoneEl.style, style);\n        return dropzoneEl;\n    }\n\n    /**\n     * Creates a dropzone covering the whole sanitized element in which we\n     * cannot drop.\n     *\n     * @returns {HTMLElement}\n     */\n    createSanitizedDropzone() {\n        const dropzoneEl = this.document.createElement(\"div\");\n        dropzoneEl.classList.add(\n            \"oe_drop_zone\",\n            \"oe_insert\",\n            \"oe_sanitized_drop_zone\",\n            \"text-center\",\n            \"text-uppercase\"\n        );\n        const messageEl = this.document.createElement(\"p\");\n        messageEl.textContent = _t(\"For technical reasons, this block cannot be dropped here\");\n        dropzoneEl.prepend(messageEl);\n        return dropzoneEl;\n    }\n\n    /**\n     * Creates a dropzone taking the entire area of the given row in grid mode.\n     * It will allow to place the elements dragged over it inside the grid it\n     * belongs to.\n     *\n     * @param {Element} rowEl\n     * @returns {HTMLElement}\n     */\n    createGridDropzone(rowEl) {\n        const columnCount = 12;\n        const rowCount = parseInt(rowEl.dataset.rowCount);\n        const dropzoneEl = this.document.createElement(\"div\");\n        dropzoneEl.classList.add(\"oe_drop_zone\", \"oe_insert\", \"oe_grid_zone\");\n        Object.assign(dropzoneEl.style, {\n            gridArea: 1 + \"/\" + 1 + \"/\" + (rowCount + 1) + \"/\" + (columnCount + 1),\n            minHeight: window.getComputedStyle(rowEl).height,\n            width: window.getComputedStyle(rowEl).width,\n        });\n        return dropzoneEl;\n    }\n\n    /**\n     * Checks whether the dropzone to insert should be horizontal or vertical.\n     *\n     * @param {HTMLElement} hookEl the element before/after which the dropzone\n     *   will be inserted\n     * @param {HTMLElement} parentEl the parent element of `hookEl`\n     * @param {Boolean} toInsertInline true if the dragged element is inline\n     * @returns {Object} - `vertical[Boolean]`: true if the dropzone is vertical\n     *                   - `style[Object]`: the style to add to the dropzone\n     */\n    setDropzoneDirection(hookEl, parentEl, toInsertInline) {\n        let vertical = false;\n        const style = {};\n        const hookStyle = window.getComputedStyle(hookEl);\n        const parentStyle = window.getComputedStyle(parentEl);\n\n        const float = hookStyle.float || hookStyle.cssFloat;\n        const { display, flexDirection } = parentStyle;\n\n        if (\n            toInsertInline ||\n            float === \"left\" ||\n            float === \"right\" ||\n            (display === \"flex\" && flexDirection === \"row\")\n        ) {\n            if (!toInsertInline) {\n                style.float = float;\n            }\n            // Compute the parent content width and the element outer width.\n            const parentPaddingX =\n                parseFloat(parentStyle.paddingLeft) + parseFloat(parentStyle.paddingRight);\n            const parentBorderX =\n                parseFloat(parentStyle.borderLeft) + parseFloat(parentStyle.borderRight);\n            const hookMarginX =\n                parseFloat(hookStyle.marginLeft) + parseFloat(hookStyle.marginRight);\n\n            const parentContentWidth =\n                parentEl.getBoundingClientRect().width - parentPaddingX - parentBorderX;\n            const hookOuterWidth = hookEl.getBoundingClientRect().width + hookMarginX;\n\n            if (parseInt(parentContentWidth) !== parseInt(hookOuterWidth)) {\n                vertical = true;\n                const hookOuterHeight = hookEl.getBoundingClientRect().height;\n                style.height = Math.max(hookOuterHeight, 30) + \"px\";\n                if (toInsertInline) {\n                    style.display = \"inline-block\";\n                    style.verticalAlign = \"middle\";\n                    style.float = \"none\";\n                }\n            }\n        }\n\n        return { vertical, style };\n    }\n\n    /**\n     * @typedef Selectors\n     * @property {Set<HTMLElement>} selectorSiblings elements which must have\n     *   siblings dropzones\n     * @property {Set<HTMLElement>} selectorChildren elements which must have\n     *   child dropzones between each existing child\n     * @property {Set<HTMLElement>} selectorSanitized sanitized elements in\n     *   which an indicative dropzone preventing the drop must be inserted\n     * @property {Set<HTMLElement>} selectorGrids elements which are in grid\n     *   mode and for which a grid dropzone must be inserted\n     */\n    /**\n     * @typedef Options\n     * @property {Boolean} toInsertInline true if the dragged element is inline\n     * @property {Boolean}isContentInIframe true if the content is inside an\n     * iframe\n     */\n    /**\n     * Creates dropzones in the DOM (= locations where dragged elements may be\n     * dropped).\n     *\n     * @param {Selectors} selectors\n     * @param {Options} options\n     * @returns\n     */\n    activateDropzones(\n        { selectorSiblings, selectorChildren, selectorSanitized, selectorGrids },\n        { toInsertInline, isContentInIframe = true } = {}\n    ) {\n        const isIgnored = (el) => el.matches(\".o_we_no_overlay\") || !isVisible(el);\n        let hookEls = [];\n        for (const parentEl of selectorChildren) {\n            const validChildrenEls = [...parentEl.children].filter((el) => !isIgnored(el));\n            hookEls.push(...validChildrenEls);\n            parentEl.prepend(this.createDropzone(parentEl));\n        }\n        hookEls.push(...selectorSiblings);\n        const systemNodeSelectors = this.getResource(\"system_node_selectors\").join(\",\");\n        if (systemNodeSelectors) {\n            hookEls = hookEls.filter((el) => !closestElement(el, systemNodeSelectors));\n        }\n\n        // Inserting the normal dropzones.\n        for (const hookEl of hookEls) {\n            const parentEl = hookEl.parentElement;\n            const { vertical, style } = this.setDropzoneDirection(hookEl, parentEl, toInsertInline);\n\n            let previousEl = hookEl.previousElementSibling;\n            while (previousEl && isIgnored(previousEl)) {\n                previousEl = previousEl.previousElementSibling;\n            }\n            if (!previousEl || !previousEl.classList.contains(\"oe_drop_zone\")) {\n                hookEl.before(this.createDropzone(parentEl, vertical, style));\n            }\n\n            if (hookEl.classList.contains(\"oe_drop_clone\")) {\n                continue;\n            }\n\n            let nextEl = hookEl.nextElementSibling;\n            while (nextEl && isIgnored(nextEl)) {\n                nextEl = nextEl.nextElementSibling;\n            }\n            if (!nextEl || !nextEl.classList.contains(\"oe_drop_zone\")) {\n                hookEl.after(this.createDropzone(parentEl, vertical, style));\n            }\n        }\n\n        // Inserting a sanitized dropzone for each sanitized area.\n        for (const sanitizedZoneEl of selectorSanitized) {\n            sanitizedZoneEl.style.position = \"relative\";\n            sanitizedZoneEl.prepend(this.createSanitizedDropzone());\n        }\n        this.sanitizedZoneEls = selectorSanitized;\n\n        // Inserting a grid dropzone for each row in grid mode.\n        for (const rowEl of selectorGrids) {\n            rowEl.append(this.createGridDropzone(rowEl));\n        }\n\n        // In the case where the editable content is in an iframe, take the\n        // iframe offset into account to compute the dropzones.\n        if (isContentInIframe) {\n            const dropzoneEls = [...this.editable.querySelectorAll(\".oe_drop_zone\")];\n            dropzoneEls.forEach((dropzoneEl) => {\n                dropzoneEl.oldGetBoundingRect = dropzoneEl.getBoundingClientRect;\n                dropzoneEl.getBoundingClientRect = () => {\n                    // iframeRect should be re-computed every time in case\n                    // the iframe is inside a scrollable element which can\n                    // be scrolled during the drag&drop operation.\n                    const iframeRect = this.iframe.getBoundingClientRect();\n                    const rect = dropzoneEl.oldGetBoundingRect();\n                    rect.x += iframeRect.x;\n                    rect.y += iframeRect.y;\n                    return rect;\n                };\n            });\n        }\n\n        return [...this.editable.querySelectorAll(\".oe_drop_zone:not(.oe_sanitized_drop_zone)\")];\n    }\n\n    /**\n     * Removes all the dropzones.\n     */\n    removeDropzones() {\n        this.editable.querySelectorAll(\".oe_drop_zone\").forEach((dropzoneEl) => {\n            dropzoneEl.remove();\n        });\n        this.sanitizedZoneEls.forEach((sanitizedZoneEl) =>\n            sanitizedZoneEl.style.removeProperty(\"position\")\n        );\n        this.sanitizedZoneEls = [];\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\n\n/** @typedef {import(\"plugins\").CSSSelector} CSSSelector */\n/**\n * @typedef {CSSSelector[]} so_content_addition_selector\n * @typedef {CSSSelector[]} so_snippet_addition_selector\n */\n\nconst card_parent_handlers =\n    \".s_three_columns .row > div, .s_comparisons .row > div, .s_cards_grid .row > div, .s_cards_soft .row > div, .s_product_list .row > div, .s_newsletter_centered .row > div, .s_company_team_spotlight .row > div, .s_comparisons_horizontal .row > div, .s_company_team_grid .row > div, .s_company_team_card .row > div, .s_carousel_cards_item\";\nconst special_cards_selector = `.s_card.s_timeline_card, div:is(${card_parent_handlers}) > .s_card`;\n\nconst so_snippet_addition_drop_in =\n    \":not(p).oe_structure:not(.oe_structure_solo), :not(.o_mega_menu):not(p)[data-oe-type=html], :not(p).oe_structure.oe_structure_solo:not(:has(> section:not(.s_snippet_group), > div:not(.o_hook_drop_zone)))\";\n\n// TODO need to split by addons\n\nexport class DropZoneSelectorPlugin extends Plugin {\n    static id = \"dropzone_selector\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        dropzone_selector: [\n            {\n                selector: \".accordion > .accordion-item\",\n                dropIn: \".accordion:has(> .accordion-item)\",\n            },\n            {\n                plugin: this,\n                get selector() {\n                    return this.plugin.getResource(\"so_snippet_addition_selector\").join(\", \");\n                },\n                dropIn: so_snippet_addition_drop_in,\n            },\n            {\n                plugin: this,\n                get selector() {\n                    return [\n                        ...this.plugin.getResource(\"so_content_addition_selector\"),\n                        \".s_card\",\n                    ].join(\", \");\n                },\n                exclude: `${special_cards_selector}`,\n                dropIn: \"nav, .row.o_grid_mode\",\n                get dropNear() {\n                    return `p, h1, h2, h3, ul, ol, div:not(.o_grid_item_image) > img, div:not(.o_grid_item_image) > a, .btn, ${this.plugin\n                        .getResource(\"so_content_addition_selector\")\n                        .join(\", \")}, .s_card:not(${special_cards_selector})`;\n                },\n                excludeNearParent: so_snippet_addition_drop_in,\n            },\n            {\n                selector: \".row > div\",\n                exclude: \".s_col_no_resize.row > div, .s_col_no_resize\",\n                dropNear: \".row:not(.s_col_no_resize) > div\",\n            },\n            {\n                selector: \".row > div\",\n                exclude: \".s_col_no_resize.row > div, .s_col_no_resize\",\n                dropNear: \".row.o_grid_mode > div\",\n            },\n        ],\n        so_snippet_addition_selector: [\"section\", \".parallax\", \".s_hr\"],\n        so_content_addition_selector: [\n            \"blockquote\",\n            \".s_text_highlight\",\n            \".s_donation\", // TODO: move to plugin\n            \".o_snippet_drop_in_only\",\n        ],\n    };\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\n/**\n * @typedef {((arg: { sourceEl: HTMLElement, targetEl: HTMLElement }) => void)[]} after_replication_handlers\n */\n\nexport class FieldChangeReplicationPlugin extends Plugin {\n    static id = \"fieldChangeReplication\";\n    static dependencies = [\"dom\"];\n\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        handleNewRecords: this.handleMutations.bind(this),\n        normalize_handlers: withSequence(9000, this.normalizeHandler.bind(this)),\n    };\n\n    setup() {\n        this.fieldsToReplicate = new Set();\n    }\n\n    /**\n     * @param { import(\"@html_editor/core/history_plugin\").HistoryMutationRecord[] } records\n     */\n    handleMutations(records) {\n        records\n            .filter((r) => !(r.type === \"attributes\" && r.attributeName.startsWith(\"data-oe-t\")))\n            .map((r) =>\n                closestElement(r.target, \"[data-oe-model], [data-oe-translation-source-sha]\")\n            )\n            .filter(Boolean)\n            // Do not forward \"unstyled\" copies to other nodes.\n            .filter((fieldEl) => !fieldEl.classList.contains(\"o_translation_without_style\"))\n            .forEach((fieldEl) => this.fieldsToReplicate.add(fieldEl));\n    }\n\n    /**\n     * @param { Node } commonAncestor\n     * @param { \"original\"|\"undo\"|\"redo\"|\"restore\" } stepState\n     */\n    normalizeHandler(commonAncestor, stepState) {\n        const fields = this.fieldsToReplicate;\n        this.fieldsToReplicate = new Set();\n        if (stepState !== \"original\") {\n            return;\n        }\n        const touchedEls = new Set();\n        for (const sourceEl of fields) {\n            const same = (attribute, quote = '\"') =>\n                `[${attribute}=${quote}${sourceEl.getAttribute(attribute)}${quote}]`;\n            let selector = \"\";\n            if (sourceEl.getAttribute(\"data-oe-model\")) {\n                selector += same(\"data-oe-model\") + same(\"data-oe-id\") + same(\"data-oe-field\");\n            }\n            if (sourceEl.getAttribute(\"data-oe-translation-source-sha\")) {\n                selector += same(\"data-oe-translation-source-sha\");\n            }\n            if (sourceEl.getAttribute(\"data-oe-type\")) {\n                selector += same(\"data-oe-type\");\n            }\n            if (sourceEl.getAttribute(\"data-oe-expression\")) {\n                selector += same(\"data-oe-expression\");\n            } else if (sourceEl.getAttribute(\"data-oe-xpath\")) {\n                selector += same(\"data-oe-xpath\");\n            }\n            if (sourceEl.getAttribute(\"data-oe-contact-options\")) {\n                selector += same(\"data-oe-contact-options\", \"'\");\n            }\n\n            if (sourceEl.getAttribute(\"data-oe-type\") === \"many2one\") {\n                selector +=\n                    \",[data-oe-model]\" +\n                    same(\"data-oe-type\") +\n                    same(\"data-oe-many2one-model\") +\n                    same(\"data-oe-many2one-id\");\n                selector +=\n                    \",[data-oe-model][data-oe-field=name]\" +\n                    `[data-oe-model=\"${sourceEl.getAttribute(\"data-oe-many2one-model\")}\"]` +\n                    `[data-oe-id=\"${sourceEl.getAttribute(\"data-oe-many2one-id\")}\"]`;\n            }\n\n            if (sourceEl.getAttribute(\"data-oe-field\") === \"name\") {\n                selector +=\n                    \",[data-oe-model][data-oe-type=many2one]\" +\n                    `[data-oe-many2one-model=\"${sourceEl.getAttribute(\"data-oe-model\")}\"]` +\n                    `[data-oe-many2one-id=\"${sourceEl.getAttribute(\"data-oe-id\")}\"]`;\n            }\n\n            const targetEls = [...this.editable.querySelectorAll(selector)].filter(\n                (targetEl) => targetEl !== sourceEl\n            );\n            if (targetEls.length) {\n                const cloneEl = sourceEl.cloneNode(true);\n                this.dependencies.dom.removeSystemProperties(cloneEl);\n                this.dispatchTo(\"clean_for_save_handlers\", { root: cloneEl });\n                for (const targetEl of targetEls) {\n                    if (targetEl.classList.contains(\"o_translation_without_style\")) {\n                        // For generated elements such as the navigation\n                        // labels of website's table of content, only the\n                        // text of the referenced translation must be used.\n                        if (targetEl.innerText !== cloneEl.innerText) {\n                            targetEl.innerText = cloneEl.innerText;\n                            touchedEls.add(targetEl);\n                        }\n                    } else {\n                        if (targetEl.innerHTML !== cloneEl.innerHTML) {\n                            targetEl.replaceChildren(...cloneEl.cloneNode(true).childNodes);\n                            touchedEls.add(targetEl);\n                        }\n                    }\n                    this.dispatchTo(\"after_replication_handlers\", { sourceEl, targetEl });\n                }\n            }\n        }\n        for (const touchedEl of touchedEls) {\n            this.dispatchTo(\"normalize_handlers\", touchedEl);\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { clamp } from \"@web/core/utils/numbers\";\nimport {\n    addBackgroundGrid,\n    additionalRowLimit,\n    checkIfImageColumn,\n    cleanUpGrid,\n    convertColumnToGrid,\n    convertToNormalColumn,\n    getGridItemProperties,\n    getGridProperties,\n    resizeGrid,\n    setElementToMaxZindex,\n    toggleGridMode,\n} from \"@html_builder/utils/grid_layout_utils\";\nimport { isElement } from \"@html_editor/utils/dom_info\";\n\nconst gridItemSelector = \".row.o_grid_mode > div.o_grid_item\";\n\nfunction isGridItem(el) {\n    return el.matches(gridItemSelector);\n}\n\nexport class GridLayoutPlugin extends Plugin {\n    static id = \"gridLayout\";\n    static dependencies = [\"selection\", \"builderOptions\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        get_overlay_buttons: withSequence(0, {\n            getButtons: this.getActiveOverlayButtons.bind(this),\n        }),\n        on_cloned_handlers: this.onCloned.bind(this),\n        on_removed_handlers: this.onRemoved.bind(this),\n        // Drag and drop from sidebar\n        on_snippet_dragged_handlers: this.onSnippetDragged.bind(this),\n        on_snippet_over_dropzone_handlers: this.onSnippetOverDropzone.bind(this),\n        on_snippet_move_handlers: this.onSnippetMove.bind(this),\n        on_snippet_out_dropzone_handlers: this.onSnippetOutDropzone.bind(this),\n        on_snippet_dropped_over_handlers: this.onSnippetDroppedOver.bind(this),\n        on_snippet_dropped_near_handlers: this.onSnippetDroppedNear.bind(this),\n        on_snippet_dropped_handlers: withSequence(1000, this.onSnippetDropped.bind(this)),\n        // Drag and drop from the page\n        is_draggable_handlers: this.isDraggable.bind(this),\n        on_element_dragged_handlers: this.onElementDragged.bind(this),\n        on_element_over_dropzone_handlers: this.onDropzoneOver.bind(this),\n        on_element_move_handlers: this.onDragMove.bind(this),\n        on_element_out_dropzone_handlers: this.onDropzoneOut.bind(this),\n        on_element_dropped_over_handlers: this.onElementDroppedOver.bind(this),\n        on_element_dropped_near_handlers: this.onElementDroppedNear.bind(this),\n        on_element_dropped_handlers: this.onElementDropped.bind(this),\n        // Ignore background grid in history\n        savable_mutation_record_predicates: this.ignoreBackgroundGrid.bind(this),\n    };\n\n    setup() {\n        this.overlayTarget = null;\n        this.isRtl = this.config.isEditableRTL;\n        this.iframe = this.document.defaultView.frameElement;\n    }\n\n    /**\n     * Checks if the given container element has the grid mode option.\n     *\n     * @param {HTMLElement} containerEl the container element\n     * @returns {Boolean}\n     */\n    hasGridLayoutOption(containerEl) {\n        // Check if the \"LayoutOption\" option is active.\n        const { targetEl } = this.dependencies.builderOptions.findOption(\n            containerEl,\n            \"LayoutOption\",\n            true\n        );\n        return !!targetEl && targetEl === containerEl;\n    }\n\n    ignoreBackgroundGrid(record) {\n        if (record.type === \"childList\") {\n            const addedOrRemovedNode = (record.addedTrees[0] || record.removedTrees[0]).node;\n            // Do not record the addition/removal of the background grid.\n            if (\n                isElement(addedOrRemovedNode) &&\n                addedOrRemovedNode.matches(\".o_we_background_grid\")\n            ) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    getActiveOverlayButtons(target) {\n        if (!isGridItem(target)) {\n            this.overlayTarget = null;\n            return [];\n        }\n\n        const buttons = [];\n        this.overlayTarget = target;\n        if (!this.config.isMobileView(this.overlayTarget)) {\n            buttons.push(\n                {\n                    class: \"o_send_back oi\",\n                    title: _t(\"Send to back\"),\n                    handler: this.sendGridItemToBack.bind(this),\n                },\n                {\n                    class: \"o_bring_front oi\",\n                    title: _t(\"Bring to front\"),\n                    handler: this.bringGridItemToFront.bind(this),\n                }\n            );\n        }\n        return buttons;\n    }\n\n    onCloned({ cloneEl }) {\n        if (isGridItem(cloneEl)) {\n            // If it is a grid item, shift the clone by one cell to the right\n            // and to the bottom, wrap to the first column if we reached the\n            // last one.\n            let { rowStart, rowEnd, columnStart, columnEnd } = getGridItemProperties(cloneEl);\n            const columnSpan = columnEnd - columnStart;\n            columnStart = columnEnd === 13 ? 1 : columnStart + 1;\n            columnEnd = columnStart + columnSpan;\n            const newGridArea = `${rowStart + 1} / ${columnStart} / ${rowEnd + 1} / ${columnEnd}`;\n            cloneEl.style.gridArea = newGridArea;\n\n            // Update the z-index and the grid row count.\n            const rowEl = cloneEl.parentElement;\n            setElementToMaxZindex(cloneEl, rowEl);\n            resizeGrid(rowEl);\n        }\n    }\n\n    onRemoved({ nextTargetEl }) {\n        // Resize the grid, if any, to have the correct row count.\n        // If the active element after a removal is a grid item, it means we\n        // potentially removed a sibling grid item.\n        if (nextTargetEl.classList.contains(\"o_grid_item\")) {\n            resizeGrid(nextTargetEl.parentElement);\n        }\n    }\n\n    /**\n     * Puts the grid item behind all the others (minimum z-index).\n     */\n    sendGridItemToBack() {\n        const rowEl = this.overlayTarget.parentNode;\n        const columnEls = [...rowEl.children].filter((el) => el !== this.overlayTarget);\n        const minZindex = Math.min(...columnEls.map((el) => el.style.zIndex));\n\n        // While the minimum z-index is not 0, it is OK to decrease it and to\n        // set the column to it. Otherwise, the column is set to 0 and the\n        // other columns z-index are increased by one.\n        if (minZindex > 0) {\n            this.overlayTarget.style.zIndex = minZindex - 1;\n        } else {\n            columnEls.forEach((columnEl) => columnEl.style.zIndex++);\n            this.overlayTarget.style.zIndex = 0;\n        }\n    }\n\n    /**\n     * Puts the grid item in front all the others (maximum z-index).\n     */\n    bringGridItemToFront() {\n        const rowEl = this.overlayTarget.parentNode;\n        setElementToMaxZindex(this.overlayTarget, rowEl);\n    }\n\n    /**\n     * Adapts the height of the grid item (if any) to its content and returns a\n     * function restoring the grid item height.\n     *\n     * @param {HTMLElement} el the new content\n     * @param {Boolean} [shouldResizeGrid=true] `true` if the grid height should also\n     *   be adapted.\n     * @returns {Function} a function restoring the grid item state\n     */\n    adjustGridItem(el, shouldResizeGrid = true) {\n        const gridItemEl = el.closest(\".o_grid_item\");\n        if (gridItemEl && gridItemEl !== el && !this.config.isMobileView(gridItemEl)) {\n            const rowEl = gridItemEl.parentElement;\n            const { rowGap, rowSize } = getGridProperties(rowEl);\n            const { rowStart, rowEnd } = getGridItemProperties(gridItemEl);\n            const oldRowSpan = rowEnd - rowStart;\n\n            // Compute the new height.\n            const { borderTop, borderBottom, paddingTop, paddingBottom } =\n                window.getComputedStyle(gridItemEl);\n            const borderY = parseFloat(borderTop) + parseFloat(borderBottom);\n            const paddingY = parseFloat(paddingTop) + parseFloat(paddingBottom);\n            const height = gridItemEl.scrollHeight + borderY + paddingY;\n\n            const rowSpan = Math.ceil((height + rowGap) / (rowSize + rowGap));\n            gridItemEl.style.gridRowEnd = rowStart + rowSpan;\n            gridItemEl.classList.remove(`g-height-${oldRowSpan}`);\n            gridItemEl.classList.add(`g-height-${rowSpan}`);\n            if (shouldResizeGrid) {\n                resizeGrid(rowEl);\n            }\n\n            return () => {\n                // Restore the grid item height.\n                gridItemEl.style.gridRowEnd = rowEnd;\n                gridItemEl.classList.remove(`g-height-${rowSpan}`);\n                gridItemEl.classList.add(`g-height-${oldRowSpan}`);\n                if (shouldResizeGrid) {\n                    resizeGrid(rowEl);\n                }\n            };\n        }\n\n        return () => {};\n    }\n\n    //--------------------------------------------------------------------------\n    // DRAG AND DROP HANDLERS (from the sidebar)\n    //--------------------------------------------------------------------------\n\n    /**\n     * Returns the grid dropzone that overlaps with the given dropzone, if any.\n     *\n     * @param {HTMLElement} dropzoneEl the dropzone\n     * @returns {HTMLElement}\n     */\n    getOverlappingGridDropzone(dropzoneEl) {\n        const closestGridEl = dropzoneEl.closest(\".o_grid_mode\");\n        const gridDropzoneEl = closestGridEl && closestGridEl.querySelector(\".oe_grid_zone\");\n        return gridDropzoneEl;\n    }\n\n    /**\n     * Wraps the given element inside a grid item, makes the drag and drop use\n     * this item instead and returns it.\n     *\n     * @param {HTMLElement} el the element\n     * @param {HTMLElement} dropzoneEl the grid dropzone\n     * @param {Object} dragState the current drag state\n     * @returns {HTMLElement}\n     */\n    wrapInGridItem(el, dropzoneEl, dragState) {\n        // Create the grid item.\n        const columnEl = document.createElement(\"div\");\n        const columnSpan = dragState.snippet.gridColumnSpan || 6;\n        columnEl.classList.add(\n            \"o_grid_item\",\n            `col-lg-${columnSpan}`,\n            `g-col-lg-${columnSpan}`,\n            \"g-height-1\"\n        );\n        columnEl.style.gridArea = `1 / 1 / 2 / ${columnSpan + 1}`;\n        dropzoneEl.after(columnEl);\n        columnEl.append(el);\n        dragState.draggedEl = columnEl;\n\n        // Remove the padding/margins.\n        const { paddingToRemove, marginToRemove, marginToAdd } = dragState;\n        el.classList.remove(...paddingToRemove, ...marginToRemove);\n        el.classList.add(...marginToAdd);\n\n        // Adjust the grid item dimensions to its content and store them.\n        this.adjustGridItem(el, false);\n        const { rowStart, rowEnd } = getGridItemProperties(columnEl);\n        dragState.columnSpan = columnSpan;\n        dragState.rowSpan = rowEnd - rowStart;\n        return columnEl;\n    }\n\n    /**\n     * Called when we start dragging the snippet.\n     *\n     * @param {Object} - snippetEl: the dragged snippet\n     *                 - dragState: the current drag state\n     */\n    onSnippetDragged({ snippetEl, dragState }) {\n        // Store the padding and margin classes to remove/add them when needed.\n        const paddingRegex = /^((pt|pb)\\d{1,3}$)/;\n        const marginRegex = /^((mt|my)-\\d$)/;\n        const paddingClasses = [...snippetEl.classList].filter((c) => paddingRegex.test(c));\n        const marginClasses = [...snippetEl.classList].filter((c) => marginRegex.test(c));\n        Object.assign(dragState, {\n            paddingToRemove: paddingClasses,\n            marginToRemove: marginClasses,\n        });\n\n        // Check if CSS rules are potentially applying a top margin.\n        snippetEl.classList.remove(...paddingClasses, ...marginClasses);\n        this.document.body.appendChild(snippetEl);\n        const { marginTop } = getComputedStyle(snippetEl);\n        dragState.marginToAdd = parseInt(marginTop) ? [\"mt-0\"] : [];\n        snippetEl.remove();\n        snippetEl.classList.add(...paddingClasses, ...paddingClasses);\n    }\n\n    /**\n     * Called when the snippet is dragged over a dropzone.\n     *\n     * @param {Object}\n     */\n    onSnippetOverDropzone({ snippetEl, dragState }) {\n        const dropzoneEl = dragState.currentDropzoneEl;\n        if (!dropzoneEl.classList.contains(\"oe_grid_zone\")) {\n            // If we are not over a grid dropzone, hide the grid dropzone that\n            // is overlapping with it (if any).\n            const gridDropzoneEl = this.getOverlappingGridDropzone(dropzoneEl);\n            if (gridDropzoneEl) {\n                gridDropzoneEl.classList.add(\"invisible\");\n            }\n        } else {\n            // If we are over a grid dropzone, wrap the snippet inside a column and\n            // drag that column instead.\n            const columnEl = this.wrapInGridItem(snippetEl, dropzoneEl, dragState);\n            // Drag the column by the middle top.\n            const iframeRect = this.iframe.getBoundingClientRect();\n            dragState.mousePositionYOnElement = 0;\n            dragState.mousePositionXOnElement =\n                (iframeRect.x + columnEl.scrollWidth / 2) * (this.isRtl ? -1 : 1);\n        }\n\n        this.onDropzoneOver({ draggedEl: dragState.draggedEl, dragState });\n    }\n\n    /**\n     * Called when the snippet is dragged out of a dropzone.\n     *\n     * @param {Object}\n     */\n    onSnippetOutDropzone({ snippetEl, dragState }) {\n        const dropzoneEl = dragState.currentDropzoneEl;\n        this.onDropzoneOut({ draggedEl: dragState.draggedEl, dragState });\n\n        if (!dropzoneEl.classList.contains(\"oe_grid_zone\")) {\n            // If we were not over a grid dropzone, show back the hidden grid\n            // dropzone if any.\n            const gridDropzoneEl = this.getOverlappingGridDropzone(dropzoneEl);\n            if (gridDropzoneEl) {\n                gridDropzoneEl.classList.remove(\"invisible\");\n            }\n        } else {\n            // If we were over a grid dropzone, unwrap the snippet.\n            dropzoneEl.after(snippetEl);\n            dragState.draggedEl.remove();\n            dragState.draggedEl = snippetEl;\n            // Restore the padding/margins.\n            const { paddingToRemove, marginToRemove, marginToAdd } = dragState;\n            snippetEl.classList.add(...paddingToRemove, ...marginToRemove);\n            snippetEl.classList.remove(...marginToAdd);\n        }\n    }\n\n    /**\n     * Called while moving the dragged element over a dropzone.\n     *\n     * @param {Object} - dragState: the current drag state.\n     *                 - x, y: the horizontal/vertical position of the helper\n     */\n    onSnippetMove({ dragState, x, y }) {\n        if (!dragState.overGrid) {\n            return;\n        }\n        this.onDragMove({ draggedEl: dragState.draggedEl, dragState, x, y });\n    }\n\n    /**\n     * Called when the snippet is dropped when over a dropzone.\n     *\n     * @param {Object} - droppedEl: the dropped element\n     *                 - dragState: the current drag state\n     */\n    onSnippetDroppedOver({ droppedEl, dragState }) {\n        const dropzoneEl = dragState.currentDropzoneEl;\n        if (!dropzoneEl.classList.contains(\"oe_grid_zone\")) {\n            return;\n        }\n        this.onElementDroppedOver({ droppedEl, dragState });\n    }\n\n    /**\n     * Called when the snippet is dropped near a dropzone.\n     *\n     * @param {Object} - dropzoneEl: the closest dropzone\n     */\n    onSnippetDroppedNear({ droppedEl, dropzoneEl, dragState }) {\n        if (!dropzoneEl.classList.contains(\"oe_grid_zone\")) {\n            return;\n        }\n        // If we are near a grid dropzone, wrap the snippet inside a column.\n        this.wrapInGridItem(droppedEl, dropzoneEl, dragState);\n        this.onElementDroppedNear({ droppedEl: dragState.draggedEl, dropzoneEl, dragState });\n    }\n\n    /**\n     * Called when the snippet is dropped in general.\n     *\n     * @param {Object}\n     */\n    onSnippetDropped({ snippetEl, dragState }) {\n        // Readjust the closest grid item (needed to compute its height without\n        // the inner dropzones).\n        if (\"restoreGridItem\" in dragState) {\n            dragState.restoreGridItem();\n            this.adjustGridItem(snippetEl);\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // DRAG AND DROP HANDLERS (from the page)\n    //--------------------------------------------------------------------------\n\n    /**\n     * Tells if the given element is draggable.\n     *\n     * @param {HTMLElement} targetEl the element\n     * @returns {Boolean}\n     */\n    isDraggable(targetEl) {\n        // The columns move handles are not visible in mobile view to prevent\n        // dragging them.\n        const isColumn = targetEl.parentElement?.classList.contains(\"row\");\n        if (isColumn && this.config.isMobileView(targetEl)) {\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * Called when we start dragging an element.\n     *\n     * @param {Object} - draggedEl: the dragged element\n     *                 - dragState: the current drag state\n     */\n    onElementDragged({ draggedEl, dragState }) {\n        const parentEl = draggedEl.parentElement;\n        const isColumn = parentEl.classList.contains(\"row\");\n        if (isColumn) {\n            const rowEl = parentEl;\n            const containerEl = rowEl.parentElement;\n            const columnEl = draggedEl;\n\n            // Allow the grid mode if the container has the option or if\n            // the grid mode is already activated.\n            const hasGridOption = this.hasGridLayoutOption(containerEl);\n            const isRowInGridMode = rowEl.classList.contains(\"o_grid_mode\");\n            const allowGridMode = hasGridOption || isRowInGridMode;\n\n            if (allowGridMode) {\n                // Toggle the grid mode if it is not already on.\n                if (!isRowInGridMode) {\n                    const preserveSelection = this.dependencies.selection.preserveSelection;\n                    toggleGridMode(containerEl, preserveSelection, this.config.mobileBreakpoint);\n                }\n                const gridItemProps = getGridItemProperties(columnEl);\n\n                // Store the grid column and row spans of the column.\n                const { columnStart, columnEnd, rowStart, rowEnd } = gridItemProps;\n                dragState.columnSpan = columnEnd - columnStart;\n                dragState.rowSpan = rowEnd - rowStart;\n\n                // Store the initial state of the column.\n                const { gridArea, zIndex } = gridItemProps;\n                dragState.startGridArea = gridArea;\n                dragState.startZindex = zIndex;\n                dragState.startGridEl = rowEl;\n            } else {\n                // If the column comes from a snippet that does not toggle the\n                // grid mode on drag, store its width and height to use them\n                // when the column goes over a grid dropzone.\n                const style = window.getComputedStyle(columnEl);\n                const { borderLeft, borderRight, borderTop, borderBottom } = style;\n                const borderX = parseFloat(borderLeft) + parseFloat(borderRight);\n                const borderY = parseFloat(borderTop) + parseFloat(borderBottom);\n                // Use the image dimension if the column only contains an image.\n                const isImageColumn = checkIfImageColumn(columnEl);\n                const sizedEl = isImageColumn ? columnEl.querySelector(\"img\") : columnEl;\n                dragState.columnWidth = sizedEl.scrollWidth + borderX;\n                dragState.columnHeight = sizedEl.scrollHeight + borderY;\n            }\n        }\n    }\n\n    /**\n     * Called when the element is dragged over a dropzone.\n     *\n     * @param {Object}\n     */\n    onDropzoneOver({ draggedEl, dragState }) {\n        const dropzoneEl = dragState.currentDropzoneEl;\n        if (!dropzoneEl.classList.contains(\"oe_grid_zone\")) {\n            // Adjust the closest grid item if any.\n            dragState.restoreGridItem = this.adjustGridItem(draggedEl);\n            return;\n        }\n\n        const rowEl = dropzoneEl.parentElement;\n        const columnEl = draggedEl;\n        // If the column does not come from a grid mode snippet, convert it to a\n        // grid item and store its dimensions.\n        if (!columnEl.classList.contains(\"o_grid_item\")) {\n            const { columnWidth, columnHeight } = dragState;\n            const spans = convertColumnToGrid(\n                rowEl,\n                columnEl,\n                columnWidth,\n                columnHeight,\n                this.config.mobileBreakpoint\n            );\n            dragState.columnSpan = spans.columnSpan;\n            dragState.rowSpan = spans.rowSpan;\n        }\n        const { columnSpan, rowSpan } = dragState;\n\n        // Create the drag helper.\n        const dragHelperEl = document.createElement(\"div\");\n        dragHelperEl.classList.add(\"o_we_drag_helper\");\n        dragHelperEl.style.gridArea = `1 / 1 / ${1 + rowSpan} / ${1 + columnSpan}`;\n        rowEl.append(dragHelperEl);\n\n        // Add the background grid and update the dropzone (in the case where\n        // the column is bigger than the grid).\n        const backgroundGridEl = addBackgroundGrid(rowEl, rowSpan);\n        const rowCount = Math.max(rowEl.dataset.rowCount, rowSpan);\n        dropzoneEl.style.gridRowEnd = rowCount + 1;\n\n        // Set the column, the background grid and the drag helper z-indexes.\n        // The grid item z-index is set to its original one if we are in its\n        // starting grid, or to the maximum z-index of the grid otherwise.\n        const { startGridEl, startZindex } = dragState;\n        if (rowEl === startGridEl) {\n            columnEl.style.zIndex = startZindex;\n        } else {\n            setElementToMaxZindex(columnEl, rowEl);\n        }\n        setElementToMaxZindex(backgroundGridEl, rowEl);\n        setElementToMaxZindex(dragHelperEl, rowEl);\n\n        // Force the column height and width to keep its size when the grid-area\n        // will be removed (as it prevents it from moving with the mouse).\n        const { rowGap, rowSize, columnGap, columnSize } = getGridProperties(rowEl);\n        const columnHeight = rowSpan * (rowSize + rowGap) - rowGap;\n        const columnWidth = columnSpan * (columnSize + columnGap) - columnGap;\n        Object.assign(columnEl.style, {\n            height: `${columnHeight}px`,\n            width: `${columnWidth}px`,\n            position: \"absolute\",\n            gridArea: \"\",\n        });\n        rowEl.style.position = \"relative\";\n\n        // Store information needed to drag over the grid.\n        Object.assign(dragState, {\n            startHeight: rowEl.clientHeight,\n            currentHeight: rowEl.clientHeight,\n            dragHelperEl,\n            backgroundGridEl,\n            overGrid: true,\n        });\n    }\n\n    /**\n     * Called when the element is dragged out of a dropzone.\n     *\n     * @param {Object}\n     */\n    onDropzoneOut({ draggedEl, dragState }) {\n        const dropzoneEl = dragState.currentDropzoneEl;\n        if (!dropzoneEl.classList.contains(\"oe_grid_zone\")) {\n            // Restore the adjusted grid item (if any).\n            if (\"restoreGridItem\" in dragState) {\n                dragState.restoreGridItem();\n                delete dragState.restoreGridItem;\n            }\n            return;\n        }\n\n        dragState.overGrid = false;\n        // Clean the grid and the column.\n        const columnEl = draggedEl;\n        const rowEl = dropzoneEl.parentElement;\n        const { dragHelperEl, backgroundGridEl } = dragState;\n        cleanUpGrid(rowEl, columnEl, dragHelperEl, backgroundGridEl);\n        columnEl.style.removeProperty(\"z-index\");\n\n        // Resize the grid and the dropzone.\n        resizeGrid(rowEl);\n        const rowCount = parseInt(rowEl.dataset.rowCount);\n        dropzoneEl.style.gridRowEnd = Math.max(rowCount + 1, 1);\n    }\n\n    /**\n     * Called when the element is dropped when over a dropzone.\n     *\n     * @param {Object} - droppedEl: the dropped element\n     *                 - dragState: the current drag state\n     */\n    onElementDroppedOver({ droppedEl, dragState }) {\n        const dropzoneEl = dragState.currentDropzoneEl;\n        const columnEl = droppedEl;\n        if (dropzoneEl.classList.contains(\"oe_grid_zone\")) {\n            dragState.overGrid = false;\n            const rowEl = dropzoneEl.parentElement;\n            const { dragHelperEl, backgroundGridEl } = dragState;\n\n            // Place the column at the same grid-area as the drag helper.\n            columnEl.style.gridArea = dragHelperEl.style.gridArea;\n\n            // Clean the grid and the column and resize the grid.\n            cleanUpGrid(rowEl, columnEl, dragHelperEl, backgroundGridEl);\n            resizeGrid(rowEl);\n        } else if (columnEl.classList.contains(\"o_grid_item\")) {\n            // Case when dropping a grid item in a non-grid dropzone.\n            convertToNormalColumn(columnEl, this.config.mobileBreakpoint);\n        }\n    }\n\n    /**\n     * Called when the element is dropped near a dropzone.\n     *\n     * @param {Object} - droppedEl: the dropped element\n     *                 - dropzoneEl: the closest dropzone\n     *                 - dragState: the current drag state\n     */\n    onElementDroppedNear({ droppedEl, dropzoneEl, dragState }) {\n        const columnEl = droppedEl;\n        if (dropzoneEl.classList.contains(\"oe_grid_zone\")) {\n            const rowEl = dropzoneEl.parentElement;\n            // If the column does not come from a grid mode snippet, convert it to a\n            // grid item and store its dimensions.\n            if (!columnEl.classList.contains(\"o_grid_item\")) {\n                const { columnWidth, columnHeight } = dragState;\n                const spans = convertColumnToGrid(\n                    rowEl,\n                    columnEl,\n                    columnWidth,\n                    columnHeight,\n                    this.config.mobileBreakpoint\n                );\n                dragState.columnSpan = spans.columnSpan;\n                dragState.rowSpan = spans.rowSpan;\n            }\n            const { columnSpan, rowSpan } = dragState;\n\n            // Place the column in the top left corner, set its z-index and\n            // resize the grid.\n            columnEl.style.gridArea = `1 / 1 / ${1 + rowSpan} / ${1 + columnSpan}`;\n            const { startGridEl, startZindex } = dragState;\n            if (rowEl === startGridEl) {\n                columnEl.style.zIndex = startZindex;\n            } else {\n                setElementToMaxZindex(columnEl, rowEl);\n            }\n            resizeGrid(rowEl);\n        } else if (columnEl.classList.contains(\"o_grid_item\")) {\n            // Case when a grid item is dropped near a non-grid dropzone.\n            convertToNormalColumn(columnEl, this.config.mobileBreakpoint);\n        }\n    }\n\n    /**\n     * Called while moving the dragged element over a dropzone.\n     *\n     * @param {Object} - droppedEl: the dropped element\n     *                 - dragState: the current drag state.\n     *                 - x, y: the horizontal/vertical position of the helper\n     */\n    onDragMove({ draggedEl, dragState, x, y }) {\n        if (!dragState.overGrid) {\n            return;\n        }\n\n        // Get the column dimensions and the grid position.\n        const columnEl = draggedEl;\n        const columnHeight = parseFloat(columnEl.style.height);\n        const columnWidth = parseFloat(columnEl.style.width);\n\n        const rowEl = columnEl.parentElement;\n        const rowRect = rowEl.getBoundingClientRect();\n        const rowTop = rowRect.top;\n        const rowLeft = rowRect.left;\n        const rowRight = rowRect.right;\n\n        // Place the column where the mouse is, without overflowing horizontally\n        // or above the top of the grid.\n        const { mousePositionYOnElement, mousePositionXOnElement } = dragState;\n        let top = y - rowTop - mousePositionYOnElement;\n\n        let left;\n        if (this.isRtl) {\n            left = rowRight - x - mousePositionXOnElement - columnWidth;\n        } else {\n            left = x - rowLeft - mousePositionXOnElement;\n        }\n\n        top = top < 0 ? 0 : top;\n        left = clamp(left, 0, rowEl.clientWidth - columnWidth);\n        const bottom = top + columnHeight;\n        columnEl.style.top = `${top}px`;\n        if (this.isRtl) {\n            columnEl.style.right = `${left}px`;\n        } else {\n            columnEl.style.left = `${left}px`;\n        }\n\n        // Compute the drag helper grid-area corresponding to the column\n        // position.\n        const { rowGap, rowSize, columnGap, columnSize } = getGridProperties(rowEl);\n        const { columnSpan, rowSpan, dragHelperEl } = dragState;\n\n        const rowStart = Math.round(top / (rowSize + rowGap)) + 1;\n        const columnStart = Math.round(left / (columnSize + columnGap)) + 1;\n        const rowEnd = rowStart + rowSpan;\n        const columnEnd = columnStart + columnSpan;\n        dragHelperEl.style.gridArea = `${rowStart} / ${columnStart} / ${rowEnd} / ${columnEnd}`;\n\n        // Update the reference heights, the dropzone and the background grid,\n        // depending on the vertical overflow/underflow.\n        const dropzoneEl = dragState.currentDropzoneEl;\n        const { startHeight, currentHeight, backgroundGridEl } = dragState;\n\n        const rowOverflow = Math.round((bottom - currentHeight) / (rowSize + rowGap));\n        const shouldUpdateRows =\n            bottom > currentHeight || (bottom <= currentHeight && bottom > startHeight);\n        const rowCount = Math.max(rowEl.dataset.rowCount, rowSpan);\n        const maxRowEnd = rowCount + additionalRowLimit + 1;\n        if (Math.abs(rowOverflow) >= 1 && shouldUpdateRows) {\n            if (rowEnd <= maxRowEnd) {\n                const newGridEnd = parseInt(dropzoneEl.style.gridRowEnd) + rowOverflow;\n                dropzoneEl.style.gridRowEnd = newGridEnd;\n                backgroundGridEl.style.gridRowEnd = newGridEnd;\n                dragState.currentHeight += rowOverflow * (rowSize + rowGap);\n            } else {\n                // Do not add new rows if we have reached the limit.\n                dropzoneEl.style.gridRowEnd = maxRowEnd;\n                backgroundGridEl.style.gridRowEnd = maxRowEnd;\n                dragState.currentHeight = (maxRowEnd - 1) * (rowSize + rowGap) - rowGap;\n            }\n        }\n    }\n\n    /**\n     * Called when the element is dropped in general.\n     *\n     * @param {Object}\n     */\n    onElementDropped({ droppedEl, dragState }) {\n        // Resize the grid from where the column came from (if any), as it may\n        // have not been resized if the column did not go over it.\n        const { startGridEl } = dragState;\n        if (startGridEl) {\n            resizeGrid(startGridEl);\n        }\n\n        // Adjust the closest grid item if any.\n        if (\"restoreGridItem\" in dragState) {\n            dragState.restoreGridItem();\n        }\n        this.adjustGridItem(droppedEl);\n\n        // The position of a grid item did not change if it is in its original\n        // grid and if it still has the same grid-area.\n        if (droppedEl.classList.contains(\"o_grid_item\")) {\n            dragState.hasSamePositionAsStart = () => {\n                const parentEl = droppedEl.parentElement;\n                const gridArea = droppedEl.style.gridArea;\n                const { startGridEl, startGridArea } = dragState;\n                return parentEl === startGridEl && gridArea === startGridArea;\n            };\n        }\n    }\n}\n", "import { IconPlugin as EditorIconPlugin } from \"@html_editor/main/media/icon_plugin\";\nimport { DISABLED_NAMESPACE } from \"@html_editor/main/toolbar/toolbar_plugin\";\n\nexport class IconPlugin extends EditorIconPlugin {\n    toolbarNamespace = DISABLED_NAMESPACE;\n}\n", "import { ImagePlugin as EditorImagePlugin } from \"@html_editor/main/media/image_plugin\";\nimport { DISABLED_NAMESPACE } from \"@html_editor/main/toolbar/toolbar_plugin\";\n\nexport class ImagePlugin extends EditorImagePlugin {\n    toolbarNamespace = DISABLED_NAMESPACE;\n}\n", "import {\n    Component,\n    onWillStart,\n    onWillUpdateProps,\n    useEffect,\n    useRef,\n    useState,\n    xml,\n} from \"@odoo/owl\";\nimport { Cache } from \"@web/core/utils/cache\";\n\nconst svgCache = new Cache(async (src) => {\n    let text;\n    try {\n        const response = await window.fetch(src);\n        text = await response.text();\n    } catch {\n        // In some tours, the tour finishes before the fetch is done\n        // and when a tour is finished, the python side will ask the\n        // browser to stop loading resources. This causes the fetch\n        // to fail and throw an error which crashes the test even\n        // though it completed successfully.\n        // So return an empty SVG to ensure everything completes\n        // correctly.\n        text = \"<svg></svg>\";\n    }\n    const parser = new window.DOMParser();\n    const xmlDoc = parser.parseFromString(text, \"text/xml\");\n    return xmlDoc.getElementsByTagName(\"svg\")[0];\n}, JSON.stringify);\n\nexport class Img extends Component {\n    static props = {\n        src: String,\n        class: { type: String, optional: true },\n        style: { type: String, optional: true },\n        alt: { type: String, optional: true },\n        attrs: { type: Object, optional: true },\n        svgCheck: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        svgCheck: true,\n    };\n    static template = xml`\n        <t t-if=\"state.loaded\">\n            <svg t-if=\"isSvg(props.src)\" t-ref=\"svg\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n                t-att-width=\"svg.width\"\n                t-att-viewBox=\"svg.viewBox\"\n                t-att-fill=\"svg.fill\"\n                class=\"hb-svg d-flex m-auto\"\n                t-att-class=\"props.class\"\n                t-att-style=\"props.style\"\n                t-att=\"props.attrs\"/>\n            <img t-else=\"\"\n                t-att-src=\"props.src\"\n                t-att-class=\"props.class\"\n                t-att-style=\"props.style\"\n                t-att-alt=\"props.alt\"\n                t-att=\"props.attrs\"/>\n        </t>\n        `;\n\n    setup() {\n        this.svgRef = useRef(\"svg\");\n        this.svg = {};\n        this.state = useState({ loaded: false });\n\n        onWillStart(async () => this.handleImgLoad(this.props.src));\n        onWillUpdateProps(async (nextProps) => {\n            if (this.props.src !== nextProps.src) {\n                await this.handleImgLoad(nextProps.src);\n            }\n        });\n        useEffect(\n            (imgLoaded) => {\n                if (imgLoaded && this.isSvg(this.props.src) && this.svg.children.length) {\n                    // We can't use t-out with markup because it is parsed as HTML,\n                    // but SVG need to be parsed as XML for all features to work.\n                    const children = [];\n                    for (const child of this.svg.children) {\n                        children.push(child.cloneNode(true));\n                    }\n                    this.svgRef.el.replaceChildren(...children);\n                }\n            },\n            () => [this.state.loaded]\n        );\n    }\n\n    async handleImgLoad(src) {\n        const prom = this.isSvg(src) ? this.getSvg() : this.loadImage(src);\n        if (this.isSvg(src)) {\n            prom.then((svg) => {\n                this.svg = svg;\n            });\n        }\n        if (this.env.imgGroup) {\n            this.env.imgGroup.addImgProm(prom);\n            this.env.imgGroup.loaded.then(() => {\n                this.state.loaded = true;\n            });\n        } else {\n            await prom;\n            this.state.loaded = true;\n        }\n    }\n\n    loadImage() {\n        return new Promise((resolve, reject) => {\n            const img = new Image();\n            img.onload = () => resolve({ status: \"loaded\" });\n            img.onerror = () => resolve({ status: \"error\" });\n            img.src = this.props.src;\n        });\n    }\n\n    isSvg(src) {\n        return this.props.svgCheck && src.split(\".\").pop() === \"svg\";\n    }\n\n    async getSvg() {\n        const svgEl = (await svgCache.read(this.props.src)).cloneNode(true);\n        return {\n            viewBox: svgEl.getAttribute(\"viewBox\"),\n            width: svgEl.getAttribute(\"width\") || \"\",\n            fill: svgEl.getAttribute(\"fill\") || \"\",\n            children: svgEl.children,\n        };\n    }\n}\n", "import { Component, useSubEnv, xml } from \"@odoo/owl\";\nimport { batched } from \"@web/core/utils/timing\";\n\nexport class ImgGroup extends Component {\n    static template = xml`<t><t t-slot=\"default\"/></t>`;\n    static props = {\n        slots: Object,\n    };\n\n    setup() {\n        this.load = () => {};\n        this.imgProms = [];\n        this.loadImgs = batched(this._loadImgs.bind(this));\n\n        useSubEnv({\n            imgGroup: {\n                loaded: new Promise((resolve) => {\n                    this.load = resolve;\n                }),\n                addImgProm: (promise) => {\n                    this.imgProms.push(promise);\n                    this.loadImgs();\n                },\n            },\n        });\n    }\n\n    async _loadImgs() {\n        await Promise.all(this.imgProms);\n        this.load();\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { MEDIA_SELECTOR, isProtected } from \"@html_editor/utils/dom_info\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { shouldEditableMediaBeEditable } from \"@html_builder/utils/utils_css\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Tooltip } from \"@web/core/tooltip/tooltip\";\n\n/**\n * @typedef { Object } MediaWebsiteShared\n * @property { MediaWebsitePlugin['replaceMedia'] } replaceMedia\n */\n\nexport class MediaWebsitePlugin extends Plugin {\n    static id = \"media_website\";\n    static dependencies = [\"media\", \"selection\", \"builderOptions\", \"operation\"];\n    static shared = [\"replaceMedia\"];\n\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        on_replaced_media_handlers: ({ newMediaEl }) =>\n            // Activate the new media options.\n            this.dependencies.builderOptions.setNextTarget(newMediaEl),\n        on_snippet_dropped_handlers: this.onSnippetDropped.bind(this),\n    };\n\n    setup() {\n        const basicMediaSelector = `${MEDIA_SELECTOR}, img`;\n\n        this.addDomListener(this.editable, \"dblclick\", async (ev) => {\n            const targetEl = ev.target.closest(basicMediaSelector);\n            if (!targetEl) {\n                return;\n            }\n            if (this.isReplaceableMedia(targetEl)) {\n                await this.onDblClickEditableMedia(targetEl);\n            }\n        });\n\n        this.popover = this.services.popover;\n        this.removeCurrentTooltip = () => {};\n        this.addDomListener(this.editable, \"click\", (ev) => {\n            const targetEl = ev.target.closest(basicMediaSelector);\n            if (!targetEl) {\n                return;\n            }\n            if (this.isReplaceableMedia(targetEl)) {\n                this.openImageTooltip(targetEl);\n            }\n        });\n    }\n\n    destroy() {\n        super.destroy();\n        this.removeCurrentTooltip();\n    }\n\n    /**\n     * Checks if the given media can be replaced.\n     *\n     * @param {HTMLElement} targetEl the media element\n     * @returns {Boolean}\n     */\n    isReplaceableMedia(targetEl) {\n        // For a media to be editable, the base case is to be in a\n        // container whose content is editable.\n        let isEditable = targetEl.parentElement?.isContentEditable;\n\n        if (!isEditable && targetEl.classList.contains(\"o_editable_media\")) {\n            isEditable = shouldEditableMediaBeEditable(targetEl);\n        }\n        if (\n            isEditable &&\n            !isProtected(this.dependencies.selection.getEditableSelection().anchorNode)\n        ) {\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Replaces the double-clicked media element.\n     *\n     * @param {HTMLElement} mediaEl the media element to replace\n     */\n    async onDblClickEditableMedia(mediaEl) {\n        this.dependencies.operation.next(async () => {\n            this.removeCurrentTooltip();\n            await this.replaceMedia(mediaEl);\n        });\n    }\n\n    /**\n     * Opens the media dialog to replace the selected media.\n     *\n     * @param {HTMLElement} mediaEl the media element to replace\n     */\n    async replaceMedia(mediaEl) {\n        const sel = this.dependencies.selection.getEditableSelection();\n        const editableEl =\n            closestElement(mediaEl || sel.startContainer, \".o_editable\") || this.editable;\n        await this.dependencies.media.openMediaDialog({ node: mediaEl }, editableEl);\n    }\n\n    /**\n     * Displays the \"double-click\" tooltip under the given media element.\n     *\n     * @param {HTMLElement} mediaEl the media element\n     */\n    openImageTooltip(mediaEl) {\n        // Remove the displayed tooltip if any first.\n        this.removeCurrentTooltip();\n        this.removeCurrentTooltip = this.popover.add(mediaEl, Tooltip, {\n            tooltip: _t(\"Double-click to edit\"),\n        });\n        setTimeout(this.removeCurrentTooltip, 1500);\n    }\n\n    async onSnippetDropped({ snippetEl }) {\n        if (!snippetEl.matches(\".media_iframe_video\")) {\n            return;\n        }\n        let isVideoSelected = false;\n        await new Promise((resolve) => {\n            const onClose = this.dependencies.media.openMediaDialog({\n                activeTab: \"VIDEOS\",\n                save: async (selectedVideoEl) => {\n                    isVideoSelected = true;\n                    snippetEl.insertAdjacentElement(\"afterend\", selectedVideoEl);\n                    snippetEl.remove();\n                },\n            });\n            onClose.then(() => {\n                resolve();\n            });\n        });\n        return !isVideoSelected;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    addMobileOrders,\n    fillRemovedItemGap,\n    removeMobileOrders,\n} from \"@html_builder/utils/column_layout_utils\";\nimport { isElementInViewport } from \"@html_builder/utils/utils\";\nimport { scrollTo } from \"@html_builder/utils/scrolling\";\nimport { localization } from \"@web/core/l10n/localization\";\n\n/** @typedef {import(\"plugins\").CSSSelector} CSSSelector */\n/**\n * @typedef {{\n *     selector: CSSSelector;\n *     exclude?: CSSSelector;\n *     direction: \"horizontal\" | \"vertical\";\n *     noScroll?: boolean;\n * }[]} is_movable_selector\n */\nexport class MovePlugin extends Plugin {\n    static id = \"move\";\n    static dependencies = [\"visibility\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        has_overlay_options: { hasOption: (el) => this.isMovable(el) },\n        get_overlay_buttons: withSequence(0, {\n            getButtons: this.getActiveOverlayButtons.bind(this),\n        }),\n        on_cloned_handlers: this.onCloned.bind(this),\n        on_will_remove_handlers: this.onWillRemove.bind(this),\n        on_element_dropped_handlers: this.onElementDropped.bind(this),\n        is_movable_selector: [\n            {\n                selector: \"section, .s_showcase .row .row:not(.s_col_no_resize) > div\",\n                direction: \"vertical\",\n            },\n            {\n                selector: \".row:not(.s_col_no_resize) > div\",\n                exclude: \".s_showcase .row .row > div\",\n                direction: \"horizontal\",\n            },\n        ],\n    };\n\n    setup() {\n        this.overlayTarget = null;\n        this.noScroll = false;\n        this.isEditableRTL = this.config.isEditableRTL;\n        this.isBackendRTL = localization.direction === \"rtl\";\n\n        // Compute the selectors.\n        const verticalSelector = [];\n        const verticalExclude = [];\n        const horizontalSelector = [];\n        const horizontalExclude = [];\n        const noScrollSelector = [];\n        for (const movableSelector of this.getResource(\"is_movable_selector\")) {\n            const { selector, exclude, direction, noScroll } = movableSelector;\n            if (selector) {\n                const selectors = direction === \"vertical\" ? verticalSelector : horizontalSelector;\n                selectors.push(selector);\n            }\n            if (exclude) {\n                const excludes = direction === \"vertical\" ? verticalExclude : horizontalExclude;\n                excludes.push(exclude);\n            }\n            if (noScroll) {\n                noScrollSelector.push(selector);\n            }\n        }\n\n        this.verticalMove = {\n            selector: verticalSelector.join(\", \"),\n            exclude: verticalExclude.length > 0 ? verticalExclude.join(\", \") : false,\n        };\n        this.horizontalMove = {\n            selector: horizontalSelector.join(\", \"),\n            exclude: horizontalExclude.length > 0 ? horizontalExclude.join(\", \") : false,\n        };\n        this.noScrollSelector = noScrollSelector.length > 0 ? noScrollSelector.join(\", \") : false;\n\n        // Needed for compatibility (with already dropped snippets).\n        // For each row, check if all its columns are either mobile ordered or\n        // not. If they are not consistent, then remove the mobile orders from\n        // all of them, to avoid issues.\n        const rowEls = this.editable.querySelectorAll(\".row\");\n        for (const rowEl of rowEls) {\n            const columnEls = [...rowEl.children];\n            const orderedColumnEls = columnEls.filter((el) => el.style.order);\n            if (orderedColumnEls.length && orderedColumnEls.length !== columnEls.length) {\n                removeMobileOrders(orderedColumnEls, this.config.mobileBreakpoint);\n            }\n        }\n    }\n\n    isMovable(el) {\n        return (\n            (el.matches(this.verticalMove.selector) && !el.matches(this.verticalMove.exclude)) ||\n            (el.matches(this.horizontalMove.selector) && !el.matches(this.horizontalMove.exclude))\n        );\n    }\n\n    getActiveOverlayButtons(target) {\n        if (!this.isMovable(target)) {\n            this.overlayTarget = null;\n            return [];\n        }\n\n        const buttons = [];\n        this.overlayTarget = target;\n        this.noScroll = this.overlayTarget.matches(this.noScrollSelector);\n        const reverseButtons = this.isEditableRTL !== this.isBackendRTL;\n\n        if (!this.areArrowsHidden()) {\n            const isVertical =\n                this.overlayTarget.matches(this.verticalMove.selector) &&\n                !this.overlayTarget.matches(this.verticalMove.exclude);\n            const previousSiblingEl = this.dependencies.visibility.getVisibleSibling(\n                this.overlayTarget,\n                \"prev\"\n            );\n            const nextSiblingEl = this.dependencies.visibility.getVisibleSibling(\n                this.overlayTarget,\n                \"next\"\n            );\n\n            if (previousSiblingEl) {\n                const direction = isVertical ? \"up\" : reverseButtons ? \"right\" : \"left\";\n                const button = {\n                    class: `fa fa-fw fa-angle-${direction}`,\n                    title: isVertical\n                        ? _t(\"Move up\")\n                        : this.isEditableRTL\n                        ? _t(\"Move right\")\n                        : _t(\"Move left\"),\n                    handler: this.onMoveClick.bind(this, \"prev\"),\n                };\n                buttons.push(button);\n            }\n\n            if (nextSiblingEl) {\n                const direction = isVertical ? \"down\" : reverseButtons ? \"left\" : \"right\";\n                const button = {\n                    class: `fa fa-fw fa-angle-${direction}`,\n                    title: isVertical\n                        ? _t(\"Move down\")\n                        : this.isEditableRTL\n                        ? _t(\"Move left\")\n                        : _t(\"Move right\"),\n                    handler: this.onMoveClick.bind(this, \"next\"),\n                };\n                buttons.push(button);\n            }\n\n            if (reverseButtons && !isVertical) {\n                buttons.reverse();\n            }\n        }\n        return buttons;\n    }\n\n    onCloned({ cloneEl, originalEl }) {\n        if (!this.isMovable(originalEl)) {\n            return;\n        }\n        // If there is a mobile order, the clone must have an order different\n        // than the existing ones.\n        const hasMobileOrder = !!originalEl.style.order;\n        if (hasMobileOrder) {\n            const siblingEls = [...originalEl.parentNode.children];\n            const maxOrder = Math.max(...siblingEls.map((el) => el.style.order));\n            cloneEl.style.order = maxOrder + 1;\n        }\n    }\n\n    onWillRemove(toRemoveEl) {\n        if (!this.isMovable(toRemoveEl)) {\n            return;\n        }\n        // If there is a mobile order, the gap created by the removed element\n        // must be filled in.\n        const mobileOrder = toRemoveEl.style.order;\n        if (mobileOrder) {\n            fillRemovedItemGap(toRemoveEl.parentElement, parseInt(mobileOrder));\n        }\n    }\n\n    onElementDropped({ droppedEl, dragState }) {\n        if (!this.isMovable(droppedEl)) {\n            return;\n        }\n        const parentEl = droppedEl.parentElement;\n\n        // If the dropped element has a mobile order and if it was dropped in\n        // another snippet, fill the gap left in the starting snippet.\n        const mobileOrder = droppedEl.style.order;\n        const { startParentEl } = dragState;\n        if (mobileOrder && parentEl !== startParentEl) {\n            fillRemovedItemGap(startParentEl, parseInt(mobileOrder));\n        }\n\n        // Remove all the mobile orders in the new snippet.\n        removeMobileOrders(parentEl.children, this.config.mobileBreakpoint);\n    }\n\n    areArrowsHidden() {\n        const isMobile = this.config.isMobileView(this.overlayTarget);\n        const isGridItem = this.overlayTarget.classList.contains(\"o_grid_item\");\n        const siblingsEl = [...this.overlayTarget.parentNode.children];\n        const visibleSiblingEl = siblingsEl.find(\n            (el) => el !== this.overlayTarget && window.getComputedStyle(el).display !== \"none\"\n        );\n        // The arrows are not displayed if:\n        // - the target has no visible siblings\n        // - the target is a grid item and is not in mobile view\n        return !visibleSiblingEl || (isGridItem && !isMobile);\n    }\n\n    /**\n     * Moves the element in the given direction\n     *\n     * @param {String} direction \"prev\" or \"next\"\n     */\n    onMoveClick(direction) {\n        const isMobile = this.config.isMobileView(this.overlayTarget);\n        let hasMobileOrder = !!this.overlayTarget.style.order;\n        const parentEl = this.overlayTarget.parentNode;\n        const siblingEls = parentEl.children;\n\n        // If the target is a column, the ordering in mobile view is independent\n        // from the desktop view. If we are in mobile view, we first add the\n        // mobile order if there is none yet. In the case where we are not in\n        // mobile view, the mobile order is reset.\n        const isColumn = parentEl.classList.contains(\"row\");\n        if (isMobile && isColumn && !hasMobileOrder) {\n            addMobileOrders(siblingEls, this.config.mobileBreakpoint);\n            hasMobileOrder = true;\n        } else if (!isMobile && hasMobileOrder) {\n            removeMobileOrders(siblingEls, this.config.mobileBreakpoint);\n            hasMobileOrder = false;\n        }\n\n        const siblingEl = this.dependencies.visibility.getVisibleSibling(\n            this.overlayTarget,\n            direction\n        );\n        if (hasMobileOrder) {\n            // Swap the mobile orders.\n            const currentOrder = this.overlayTarget.style.order;\n            this.overlayTarget.style.order = siblingEl.style.order;\n            siblingEl.style.order = currentOrder;\n        } else {\n            // Swap the DOM elements.\n            siblingEl.insertAdjacentElement(\n                direction === \"prev\" ? \"beforebegin\" : \"afterend\",\n                this.overlayTarget\n            );\n        }\n\n        // Scroll to the element.\n        if (!this.noScroll && !isElementInViewport(this.overlayTarget)) {\n            const { top, height } = this.overlayTarget.getBoundingClientRect();\n            const viewportHeight = this.document.defaultView.innerHeight;\n            const heightDiff = viewportHeight - height;\n            const isBottomHidden = heightDiff < top;\n            scrollTo(this.overlayTarget, {\n                extraOffset: 50,\n                forcedOffset: isBottomHidden ? heightDiff - 50 : undefined,\n                duration: 500,\n            });\n        }\n    }\n}\n", "import { Mutex } from \"@web/core/utils/concurrency\";\n\n// TODO when making apply async:\n// - check `isDestroyed` instead of `this.editableDocument.defaultView`\n\n/**\n * @typedef OperationParams\n * @property {Function} load an async function for which the mutex should wait\n *   before executing the main function\n * @property {Boolean} cancellable tells if the operation is cancellable (if it\n *   is a preview for example)\n * @property {Function} cancelPrevious the function to run when cancelling\n * @property {Number} [cancelTime=50] TODO\n * @property {Boolean} [withLoadingEffect=true] specifies if a spinner should\n *   appear on the editable during the operation\n * @property {Number} [loadingEffectDelay=500] the delay after which the\n *   spinner should appear\n * @property {Boolean} [shouldInterceptClick=false] whether clicking while the\n *   loading screen is present should retarget the click at the end of the\n *   loading time.\n */\n\nexport class Operation {\n    constructor(editableDocument = document) {\n        this.mutex = new Mutex();\n        this.editableDocument = editableDocument;\n    }\n\n    /**\n     * Allows to execute a function in the mutex.\n     * See `OperationParams.load` to make it async.\n     *\n     * @param {Function} fn the function\n     * @param {OperationParams} params\n     * @returns {Promise<void>}\n     */\n    next(\n        fn,\n        {\n            load = () => Promise.resolve(),\n            cancellable,\n            cancelPrevious,\n            cancelTime = 50,\n            withLoadingEffect = true,\n            loadingEffectDelay = 500,\n            shouldInterceptClick = false,\n        } = {}\n    ) {\n        this.cancelPrevious?.();\n        let isCancel = false;\n        let cancelResolve;\n        this.cancelPrevious =\n            cancellable &&\n            (() => {\n                this.cancelPrevious = null;\n                isCancel = true;\n                cancelResolve?.();\n                // Cancel in the mutex to wait for the revert before the next\n                // apply.\n                this.mutex.exec(async () => {\n                    await cancelPrevious?.();\n                });\n            });\n\n        const cancelTimePromise = new Promise((resolve) => setTimeout(resolve, cancelTime));\n        const cancelLoadPromise = new Promise((resolve) => {\n            cancelResolve = resolve;\n        });\n\n        return this.mutex.exec(async () => {\n            if (isCancel) {\n                return;\n            }\n\n            const removeLoadingElement = this.addLoadingElement(\n                withLoadingEffect,\n                loadingEffectDelay,\n                shouldInterceptClick\n            );\n            const applyOperation = async () => {\n                const loadResult = await load();\n\n                if (isCancel) {\n                    return;\n                }\n                this.previousLoadResolve = null;\n\n                // Cancel the operation if the iframe has been reloaded\n                // and does not have a browsing context anymore.\n                if (!this.editableDocument.defaultView) {\n                    return;\n                }\n\n                await fn?.(loadResult);\n            };\n\n            try {\n                await Promise.race([\n                    Promise.all([cancelLoadPromise, cancelTimePromise]),\n                    applyOperation(),\n                ]);\n            } finally {\n                removeLoadingElement();\n            }\n        });\n    }\n\n    /**\n     * Adds a transparent loading screen above the editable to prevent modifying\n     * its content during an ongoing operation. Returns a callback to remove\n     * the loading screen.\n     *\n     * @param {Boolean} withLoadingEffect if true, adds a loading effect\n     * @param {Number} loadingEffectDelay delay after which the loading effect\n     *   should appear\n     * @param {Boolean} shouldInterceptClick - whether to redispatch the click\n     *   under the loading element after the end of the current operation\n     * @returns {Function}\n     */\n    addLoadingElement(withLoadingEffect, loadingEffectDelay, shouldInterceptClick) {\n        const loadingScreenEl = document.createElement(\"div\");\n        loadingScreenEl.classList.add(\n            ...[\"o_loading_screen\", \"d-flex\", \"justify-content-center\", \"align-items-center\"]\n        );\n        const spinnerEl = document.createElement(\"img\");\n        spinnerEl.setAttribute(\"src\", \"/web/static/img/spin.svg\");\n        loadingScreenEl.appendChild(spinnerEl);\n\n        let removeClickListener = () => {};\n        if (shouldInterceptClick) {\n            const onClick = (ev) => {\n                const trueTargetEls = this.editableDocument.elementsFromPoint(\n                    ev.clientX,\n                    ev.clientY\n                );\n                this.next(() => {\n                    for (const trueTargetEl of trueTargetEls) {\n                        if (trueTargetEl.isConnected) {\n                            trueTargetEl.click();\n                            break;\n                        }\n                    }\n                });\n            };\n            this.editableDocument.addEventListener(\"click\", onClick);\n            removeClickListener = () => this.editableDocument.removeEventListener(\"click\", onClick);\n        }\n\n        this.editableDocument.body.appendChild(loadingScreenEl);\n\n        // If specified, add a loading effect on that element after a delay.\n        let loadingTimeout;\n        if (withLoadingEffect) {\n            loadingTimeout = setTimeout(\n                () => loadingScreenEl.classList.add(\"o_we_ui_loading\"),\n                loadingEffectDelay\n            );\n        }\n\n        return () => {\n            if (loadingTimeout) {\n                clearTimeout(loadingTimeout);\n            }\n            removeClickListener();\n            loadingScreenEl.remove();\n        };\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { Operation } from \"./operation\";\nimport { useComponent } from \"@odoo/owl\";\n\n/** @typedef {import(\"./operation\").OperationParams} OperationParams */\n\n/**\n * @typedef { Object } OperationShared\n * @property { OperationPlugin['next'] } next\n */\n\nexport class OperationPlugin extends Plugin {\n    static id = \"operation\";\n    static dependencies = [\"history\"];\n    static shared = [\"next\"];\n\n    setup() {\n        this.operation = new Operation(this.document);\n    }\n\n    /**\n     * Executes a function (async or not) in the mutex.\n     *\n     * @param {Function} fn the function\n     * @param {OperationParams} params\n     * @returns {Promise<void>}\n     */\n    next(fn, params) {\n        return this.operation.next(fn, params);\n    }\n}\n\nexport function useOperation() {\n    const comp = useComponent();\n    return (apply, ...args) => {\n        comp.env.editor.shared.operation.next(async (...args) => {\n            await apply(...args);\n            comp.env.editor.shared.history.addStep();\n        }, ...args);\n    };\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class OverlayButtons extends Component {\n    static template = \"html_builder.OverlayButtons\";\n    static props = {\n        state: { type: Object },\n    };\n\n    setup() {\n        this.state = this.props.state;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { reactive } from \"@odoo/owl\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { getScrollingElement, getScrollingTarget } from \"@web/core/utils/scrolling\";\nimport { checkElement } from \"../builder_options_plugin\";\nimport { OverlayButtons } from \"./overlay_buttons\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\n/** @typedef {import(\"@html_builder/core/builder_options_plugin\").BuilderButtonDescriptor} BuilderButtonDescriptor */\n/**\n * @typedef { Object } OverlayButtonsShared\n * @property { OverlayButtonsPlugin['hideOverlayButtons'] } hideOverlayButtons\n * @property { OverlayButtonsPlugin['showOverlayButtons'] } showOverlayButtons\n * @property { OverlayButtonsPlugin['hideOverlayButtonsUi'] } hideOverlayButtonsUi\n * @property { OverlayButtonsPlugin['showOverlayButtonsUi'] } showOverlayButtonsUi\n */\n/**\n * @typedef {{\n *      getButtons: (target: HTMLElement) => BuilderButtonDescriptor;\n * }[]} get_overlay_buttons\n */\n\nexport class OverlayButtonsPlugin extends Plugin {\n    static id = \"overlayButtons\";\n    static dependencies = [\"selection\", \"overlay\", \"history\", \"operation\"];\n    static shared = [\n        \"hideOverlayButtons\",\n        \"showOverlayButtons\",\n        \"hideOverlayButtonsUi\",\n        \"showOverlayButtonsUi\",\n    ];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        step_added_handlers: this.refreshButtons.bind(this),\n        change_current_options_containers_listeners: this.addOverlayButtons.bind(this),\n        on_mobile_preview_clicked: withSequence(20, this.refreshButtons.bind(this)),\n    };\n\n    setup() {\n        // TODO find how to not overflow the mobile preview.\n        this.iframe = this.editable.ownerDocument.defaultView.frameElement;\n        this.overlay = this.dependencies.overlay.createOverlay(\n            OverlayButtons,\n            {\n                positionOptions: {\n                    position: \"top-middle\",\n                    onPositioned: (overlayEl, position) => {\n                        const iframeRect = this.iframe.getBoundingClientRect();\n                        if (this.target && position.top < iframeRect.top) {\n                            const targetRect = this.target.getBoundingClientRect();\n                            const newTop = iframeRect.top + targetRect.bottom + 15;\n                            position.top = newTop;\n                            overlayEl.style.top = `${newTop}px`;\n                        }\n                        return;\n                    },\n                    margin: 15,\n                    flip: false,\n                },\n                closeOnPointerdown: false,\n            },\n            // The buttons should appear under other overlays, like the link\n            // popover. The default sequence is 50.\n            { sequence: 49 }\n        );\n        this.target = null;\n        this.state = reactive({\n            isVisible: true,\n            showUi: true,\n            buttons: [],\n        });\n\n        this.resizeObserver = new ResizeObserver(() => {\n            this.overlay.updatePosition();\n        });\n\n        // TODO duplicate of builderOverlay => extract somewhere\n        // Recompute the buttons when the window is resized.\n        this.refresh = throttleForAnimation(this.refreshButtons.bind(this));\n        this.addDomListener(window, \"resize\", this.refresh);\n\n        // On keydown, hide the buttons and then show them again when the mouse\n        // moves.\n        const onMouseMoveOrDown = throttleForAnimation(() => {\n            this.showOverlayButtons();\n            this.editable.removeEventListener(\"mousemove\", onMouseMoveOrDown);\n            this.editable.removeEventListener(\"mousedown\", onMouseMoveOrDown);\n        });\n        this.addDomListener(this.editable, \"keydown\", () => {\n            this.hideOverlayButtons();\n            this.editable.addEventListener(\"mousemove\", onMouseMoveOrDown);\n            this.editable.addEventListener(\"mousedown\", onMouseMoveOrDown);\n        });\n\n        // Hide the buttons when scrolling. Show them again when the scroll is\n        // over.\n        const scrollingElement = getScrollingElement(this.document);\n        const scrollingTarget = getScrollingTarget(scrollingElement);\n        this.addDomListener(\n            scrollingTarget,\n            \"scroll\",\n            throttleForAnimation(() => {\n                this.hideOverlayButtons();\n                clearTimeout(this.scrollingTimeout);\n                this.scrollingTimeout = setTimeout(() => {\n                    this.showOverlayButtons();\n                }, 250);\n            }),\n            { capture: true }\n        );\n\n        this._cleanups.push(() => {\n            this.removeOverlayButtons();\n            this.resizeObserver.disconnect();\n        });\n    }\n\n    refreshButtons() {\n        if (!this.target) {\n            return;\n        }\n        const buttons = [];\n        for (const { getButtons, editableOnly } of this.getResource(\"get_overlay_buttons\")) {\n            if (checkElement(this.target, { editableOnly })) {\n                buttons.push(...getButtons(this.target));\n            }\n        }\n        for (const button of buttons) {\n            const handler = button.handler;\n            button.handler = (...args) => {\n                this.dependencies.operation.next(async () => {\n                    await handler(...args);\n                    this.dependencies.history.addStep();\n                });\n            };\n        }\n        this.state.buttons = buttons;\n        this.overlay.updatePosition();\n    }\n\n    hideOverlayButtons() {\n        this.state.isVisible = false;\n    }\n\n    hideOverlayButtonsUi() {\n        this.state.showUi = false;\n    }\n\n    showOverlayButtons() {\n        this.state.isVisible = true;\n    }\n\n    showOverlayButtonsUi() {\n        this.state.showUi = true;\n    }\n\n    addOverlayButtons(optionsContainer) {\n        this.removeOverlayButtons();\n\n        // Find the innermost option needing the overlay buttons.\n        const optionWithOverlayButtons = optionsContainer.findLast(\n            (option) => option.hasOverlayOptions\n        );\n        if (optionWithOverlayButtons) {\n            this.target = optionWithOverlayButtons.element;\n            this.state.isVisible = true;\n            this.refreshButtons();\n            this.overlay.open({\n                target: optionWithOverlayButtons.element,\n                closeOnPointerdown: false,\n                props: {\n                    state: this.state,\n                },\n            });\n            this.resizeObserver.observe(this.target, { box: \"border-box\" });\n        }\n    }\n\n    removeOverlayButtons() {\n        if (this.target) {\n            this.resizeObserver.unobserve(this.target);\n            this.target = null;\n        }\n        this.overlay.close();\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { unremovableNodePredicates as deletePluginPredicates } from \"@html_editor/core/delete_plugin\";\nimport { isUnremovableQWebElement as qwebPluginPredicate } from \"@html_editor/others/qweb_plugin\";\nimport { isEditable } from \"@html_builder/utils/utils\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\n\n/** @typedef {import(\"plugins\").CSSSelector} CSSSelector */\n\n/**\n * @typedef { Object } RemoveShared\n * @property { RemovePlugin['removeElement'] } removeElement\n */\n\n/**\n * @typedef {((arg: { removedEl: HTMLElement, nextTargetEl: HTMLElement }) => void)[]} on_removed_handlers\n * @typedef {((toRemoveEl: HTMLElement) => void)[]} on_will_remove_handlers\n *\n * @typedef {((el: HTMLElement) => boolean)[]} empty_node_predicates\n *\n * @typedef {CSSSelector[]} is_unremovable_selector\n */\n\nconst unremovableNodePredicates = [\n    (node) => !isEditable(node.parentNode),\n    ...deletePluginPredicates,\n    qwebPluginPredicate,\n    (node) => node.parentNode.matches('[data-oe-type=\"image\"]'),\n];\n\nexport function isRemovable(el) {\n    return !unremovableNodePredicates.some((p) => p(el));\n}\n\nexport class RemovePlugin extends Plugin {\n    static id = \"remove\";\n    static dependencies = [\"builderOptions\", \"visibility\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        get_overlay_buttons: withSequence(3, {\n            getButtons: this.getActiveOverlayButtons.bind(this),\n        }),\n        empty_node_predicates: (el) => {\n            const systemNodeSelectors = this.getResource(\"system_node_selectors\").join(\",\");\n            return (\n                el.textContent.trim() === \"\" &&\n                (!systemNodeSelectors ||\n                    [...el.children].every((child) => closestElement(child, systemNodeSelectors)))\n            );\n        },\n    };\n    static shared = [\"removeElement\"];\n\n    setup() {\n        this.overlayTarget = null;\n\n        const unremovableSelectors = [];\n        for (const unremovableSelector of this.getResource(\"is_unremovable_selector\")) {\n            unremovableSelectors.push(unremovableSelector);\n        }\n        if (unremovableSelectors.length) {\n            unremovableNodePredicates.push((node) => node.matches(unremovableSelectors.join(\", \")));\n        }\n    }\n\n    getActiveOverlayButtons(target) {\n        if (!isRemovable(target)) {\n            this.overlayTarget = null;\n            return [];\n        }\n\n        const buttons = [];\n        this.overlayTarget = target;\n        const disabledReason = this.dependencies.builderOptions.getRemoveDisabledReason(target);\n        buttons.push({\n            class: \"oe_snippet_remove bg-danger fa fa-trash\",\n            title: _t(\"Remove\"),\n            disabledReason,\n            handler: () => {\n                this.removeElement(this.overlayTarget);\n            },\n        });\n        return buttons;\n    }\n\n    isEmptyAndRemovable(el, optionsTargetEls) {\n        return (\n            this.getResource(\"empty_node_predicates\").some((predicate) => predicate(el)) &&\n            !el.classList.contains(\"oe_structure\") &&\n            !el.parentElement.classList.contains(\"carousel-item\") &&\n            (!optionsTargetEls.includes(el) ||\n                optionsTargetEls.some((targetEl) => targetEl.contains(el))) &&\n            isRemovable(el)\n        );\n    }\n\n    /**\n     * Removes the given element and updates the containers if needed.\n     *\n     * @param {HTMLElement} toRemoveEl the element to remove\n     * @param {Boolean} [updateContainers=true] true if the option containers\n     *   of the remaining element should be activated.\n     */\n    removeElement(toRemoveEl, updateContainers = true) {\n        // Get the elements having options containers.\n        const optionTargetEls = this.getOptionsContainersElements().filter((targetEl) =>\n            targetEl.contains(toRemoveEl)\n        );\n        const nextTargetEl = this.removeCurrentTarget(toRemoveEl, optionTargetEls);\n        this.dispatchTo(\"on_removed_handlers\", { removedEl: toRemoveEl, nextTargetEl });\n        if (updateContainers) {\n            this.dependencies.builderOptions.setNextTarget(nextTargetEl);\n        }\n    }\n\n    /**\n     * Removes the given element from the DOM, as well as its parents when\n     * possible, and returns the element of which the options containers should\n     * be activated afterwards.\n     *\n     * @param {HTMLElement} toRemoveEl the element to remove\n     * @param {Array<HTMLElement>} optionsTargetEls the current option\n     *   containers target elements.\n     * @returns {HTMLElement}\n     */\n    removeCurrentTarget(toRemoveEl, optionsTargetEls) {\n        this.dispatchTo(\"on_will_remove_handlers\", toRemoveEl);\n\n        // Get the parent and the previous and next visible siblings.\n        let parentEl = toRemoveEl.parentElement;\n        const previousSiblingEl = this.dependencies.visibility.getVisibleSibling(\n            toRemoveEl,\n            \"prev\"\n        );\n        const nextSiblingEl = this.dependencies.visibility.getVisibleSibling(toRemoveEl, \"next\");\n        if (parentEl.matches(\".o_editable:not(body)\")) {\n            // If we target the editable, we want to reset the selection to the\n            // body. If the editable has options, we do not want to show them.\n            parentEl = parentEl.closest(\"body\");\n        }\n\n        // Remove tooltips.\n        [toRemoveEl, ...toRemoveEl.querySelectorAll(\"*\")].forEach((el) => {\n            const tooltip = Tooltip.getInstance(el);\n            if (tooltip) {\n                tooltip.dispose();\n            }\n        });\n\n        // Remove the element.\n        toRemoveEl.remove();\n\n        // Remove potential last empty text node from the parent.\n        if (parentEl) {\n            const firstChildEl = parentEl.firstChild;\n            if (firstChildEl && !firstChildEl.tagName && firstChildEl.textContent === \" \") {\n                parentEl.removeChild(firstChildEl);\n            }\n        }\n\n        // Set the sibling as the next element to activate, if any, otherwise\n        // set it as the parent.\n        let nextTargetEl = previousSiblingEl || nextSiblingEl;\n        if (!nextTargetEl) {\n            // Remove potential ancestors (like when removing the last column of\n            // a snippet).\n            while (!optionsTargetEls.includes(parentEl)) {\n                const nextParentEl = parentEl.parentElement;\n                if (!nextParentEl) {\n                    break;\n                }\n                if (this.isEmptyAndRemovable(parentEl, optionsTargetEls)) {\n                    parentEl.remove();\n                }\n                parentEl = nextParentEl;\n            }\n\n            nextTargetEl = parentEl;\n            optionsTargetEls = optionsTargetEls.filter((targetEl) =>\n                targetEl.contains(nextTargetEl)\n            );\n            if (this.isEmptyAndRemovable(parentEl, optionsTargetEls)) {\n                nextTargetEl = this.removeCurrentTarget(parentEl, optionsTargetEls);\n            }\n        }\n\n        return nextTargetEl;\n    }\n\n    getOptionsContainersElements() {\n        return this.dependencies.builderOptions.getContainers().map((option) => option.element);\n    }\n}\n", "import { escapeTextNodes } from \"@html_builder/utils/escaping\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { groupBy } from \"@web/core/utils/arrays\";\nimport { uniqueId } from \"@web/core/utils/functions\";\n\n/** @typedef {import(\"plugins\").CSSSelector} CSSSelector */\n/**\n * @typedef { Object } SaveShared\n * @property { SavePlugin['save'] } save\n * @property { SavePlugin['saveView'] } saveView\n * @property { SavePlugin['ignoreDirty'] } ignoreDirty\n */\n\n/**\n * @typedef {(() => void)[]} after_save_handlers\n * @typedef {((el?: HTMLElement) => Promise<void>)[]} before_save_handlers\n * Called at the very beginning of the save process.\n *\n * @typedef {((el: HTMLElement) => Promise<void>)[]} save_element_handlers\n * Called when saving an element (in parallel to saving the view).\n *\n * @typedef {(() => Promise<boolean>)[]} save_handlers\n * Called at the very end of the save process.\n *\n * @typedef {((cleanedEls: HTMLElement[]) => Promise<boolean>)[]} save_elements_overrides\n *\n * @typedef {(() => HTMLElement[] | NodeList)[]} get_dirty_els\n *\n * @typedef {CSSSelector[]} savable_selectors\n */\n\nexport class SavePlugin extends Plugin {\n    static id = \"savePlugin\";\n    static shared = [\"save\", \"saveView\", \"ignoreDirty\"];\n    static dependencies = [\"history\"];\n\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        handleNewRecords: this.handleMutations.bind(this),\n        start_edition_handlers: this.startObserving.bind(this),\n        // Resource definitions:\n        savable_selectors: [\n            \"#wrapwrap .oe_structure[data-oe-xpath][data-oe-id]\",\n            \"#wrapwrap [data-oe-field]:not([data-oe-sanitize-prevent-edition])\",\n            \"#wrapwrap .s_cover[data-res-model]\",\n        ],\n        clean_for_save_handlers: [\n            // ({root}) => {\n            //     clean DOM before save (leaving edit mode)\n            //     root is the clone of a node that was o_dirty\n            // }\n        ],\n        save_element_handlers: [this.saveView.bind(this)],\n        get_dirty_els: () => this.editable.querySelectorAll(\".o_dirty\"),\n        // Do not change the sequence of this resource, it must stay the first\n        // one to avoid marking dirty when not needed during the drag and drop.\n        on_prepare_drag_handlers: withSequence(0, this.ignoreDirty.bind(this)),\n    };\n\n    setup() {\n        this.canObserve = false;\n        this.savableSelector = this.getResource(\"savable_selectors\").join(\", \");\n    }\n\n    async save({ shouldSkipAfterSaveHandlers = async () => true } = {}) {\n        let skipAfterSaveHandlers;\n        try {\n            await Promise.all(this.getResource(\"before_save_handlers\").map((handler) => handler()));\n            await this._save();\n            skipAfterSaveHandlers = await shouldSkipAfterSaveHandlers();\n        } finally {\n            if (!skipAfterSaveHandlers) {\n                this.getResource(\"after_save_handlers\").forEach((handler) => handler());\n            }\n        }\n    }\n    async _save() {\n        const dirtyEls = [];\n        for (const getDirtyEls of this.getResource(\"get_dirty_els\")) {\n            dirtyEls.push(...getDirtyEls());\n        }\n        // Group elements to save if possible\n        const groupedElements = groupBy(dirtyEls, (dirtyEl) => {\n            const model = dirtyEl.dataset.oeModel;\n            const recordId = dirtyEl.dataset.oeId;\n            const field = dirtyEl.dataset.oeField;\n\n            // There are elements which have no linked model as something\n            // special is to be done \"to save them\". In that case, do not group\n            // those elements.\n            if (!model) {\n                return uniqueId(\"special-element-to-save-\");\n            }\n\n            // Group elements which are from the same field of the same record.\n            return `${model}::${recordId}::${field}`;\n        });\n        const saveProms = Object.values(groupedElements).map(async (dirtyEls) => {\n            const cleanedEls = dirtyEls.map((dirtyEl) => {\n                dirtyEl.classList.remove(\"o_dirty\");\n                const cleanedEl = dirtyEl.cloneNode(true);\n                this.dispatchTo(\"clean_for_save_handlers\", { root: cleanedEl });\n                return cleanedEl;\n            });\n            for (const saveElementsOverride of this.getResource(\"save_elements_overrides\")) {\n                if (await saveElementsOverride(cleanedEls)) {\n                    return;\n                }\n            }\n            for (const cleanedEl of cleanedEls) {\n                for (const saveElementHandler of this.getResource(\"save_element_handlers\")) {\n                    await saveElementHandler(cleanedEl);\n                }\n            }\n        });\n        // used to track dirty out of the editable scope, like header, footer or wrapwrap\n        const willSaves = this.getResource(\"save_handlers\").map((c) => c());\n        await Promise.all(saveProms.concat(willSaves));\n        this.dependencies.history.reset();\n    }\n\n    /**\n     * Saves one (dirty) element of the page.\n     *\n     * @param {HTMLElement} el - the element to save.\n     */\n    saveView(el, delayTranslations = true) {\n        const viewID = Number(el.dataset[\"oeId\"]);\n        if (!viewID) {\n            return;\n        }\n\n        let context = {};\n        if (this.services.website) {\n            const delay = delayTranslations ? { delay_translations: true } : {};\n            context = {\n                website_id: this.services.website.currentWebsite.id,\n                lang: this.services.website.currentWebsite.metadata.lang,\n                ...delay,\n            };\n        }\n\n        escapeTextNodes(el);\n        return this.services.orm.call(\n            \"ir.ui.view\",\n            \"save\",\n            [viewID, el.outerHTML, (!el.dataset[\"oeExpression\"] && el.dataset[\"oeXpath\"]) || null],\n            { context }\n        );\n    }\n\n    startObserving() {\n        this.canObserve = true;\n    }\n    /**\n     * Handles the flag of the closest savable element to the mutation as dirty\n     *\n     * @param {Object} records - The observed mutations\n     * @param {String} currentOperation - The name of the current operation\n     */\n    handleMutations(records, currentOperation) {\n        if (!this.canObserve) {\n            return;\n        }\n        if (currentOperation === \"undo\" || currentOperation === \"redo\") {\n            // Do nothing as `o_dirty` has already been handled by the history\n            // plugin.\n            return;\n        }\n        for (const record of records) {\n            if (record.attributeName === \"contenteditable\") {\n                continue;\n            }\n            let targetEl = record.target;\n            if (!targetEl.isConnected) {\n                continue;\n            }\n            if (targetEl.nodeType !== Node.ELEMENT_NODE) {\n                targetEl = targetEl.parentElement;\n            }\n            if (!targetEl) {\n                continue;\n            }\n            const savableEl = targetEl.closest(this.savableSelector);\n            if (\n                !savableEl ||\n                savableEl.classList.contains(\"o_dirty\") ||\n                savableEl.hasAttribute(\"data-oe-readonly\")\n            ) {\n                continue;\n            }\n            savableEl.classList.add(\"o_dirty\");\n        }\n    }\n\n    /**\n     * Prevents elements to be marked as dirty until it is reactivated with the\n     * returned callback.\n     *\n     * @returns {Function}\n     */\n    ignoreDirty() {\n        this.canObserve = false;\n        return () => {\n            this.canObserve = true;\n        };\n    }\n}\n", "import { escapeTextNodes } from \"@html_builder/utils/escaping\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { markup } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst savableSelector = \"[data-snippet], a.btn\";\n// TODO `so_submit_button_selector` ?\nconst savableExclude = \".o_no_save, .s_donation_donate_btn, .s_website_form_send\";\n\n// Checks if the element can be saved as a custom snippet.\nfunction isSavable(el) {\n    return el.matches(savableSelector) && !el.matches(savableExclude);\n}\n\nexport class SaveSnippetPlugin extends Plugin {\n    static id = \"saveSnippet\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        get_options_container_top_buttons: withSequence(\n            1,\n            this.getOptionsContainerTopButtons.bind(this)\n        ),\n    };\n\n    getOptionsContainerTopButtons(el) {\n        if (!isSavable(el)) {\n            return [];\n        }\n\n        return [\n            {\n                class: \"fa fa-fw fa-save oe_snippet_save o_we_hover_warning btn o-hb-btn btn-global-color-hover\",\n                title: _t(\"Save this block to use it elsewhere\"),\n                handler: this.saveSnippet.bind(this),\n            },\n        ];\n    }\n\n    /**\n     * Execute the `before_save_handlers` on {@link snippetEl},\n     * then execute {@link callback}, and finally execute the\n     * `after_save_handlers` on {@link snippetEl}.\n     * This is used, for example, to stop the interactions before cloning a\n     * snippet, and restarting them after cloning it.\n     *\n     * @param {HTMLElement} snippetEl\n     * @param {Function} callback\n     */\n    async wrapWithBeforeAfterSaveHandlers(snippetEl, callback) {\n        await Promise.all(\n            this.getResource(\"before_save_handlers\").map((handler) => handler(snippetEl))\n        );\n        let node;\n        try {\n            node = callback();\n        } finally {\n            this.getResource(\"after_save_handlers\").forEach((handler) => handler(snippetEl));\n        }\n        return node;\n    }\n\n    async saveSnippet(el) {\n        const cleanForSaveHandlers = [\n            ...this.getResource(\"clean_for_save_handlers\"),\n            ({ root }) => escapeTextNodes(root),\n        ];\n        const savedName = await this.config.saveSnippet(\n            el,\n            cleanForSaveHandlers,\n            this.wrapWithBeforeAfterSaveHandlers.bind(this)\n        );\n        if (savedName) {\n            const message = _t(\n                \"Saved as %s. Find it in your snippets.\",\n                markup`<strong>${savedName}</strong>`\n            );\n            this.services.notification.add(message, {\n                type: \"success\",\n                autocloseDelay: 5000,\n            });\n        }\n    }\n}\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { areColsCustomized } from \"@html_builder/utils/column_layout_utils\";\n\nexport class SelectNumberColumn extends BaseOptionComponent {\n    static template = \"html_builder.SelectNumberColumn\";\n\n    setup() {\n        super.setup();\n        this.state = useDomState((editingElement) => {\n            const columnEls = editingElement.querySelector(\":scope > .row\")?.children;\n            return {\n                isCustomColumn:\n                    columnEls &&\n                    areColsCustomized(\n                        columnEls,\n                        this.env.editor.config.isMobileView(editingElement),\n                        this.env.editor.config.mobileBreakpoint\n                    ),\n                canHaveZeroColumns: editingElement.matches(\".s_allow_columns\"),\n            };\n        });\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @typedef { Object } SetupEditorShared\n * @property { SetupEditorPlugin['getEditableAreas'] } getEditableAreas\n */\n\n/**\n * @typedef {(() => void | true)[]} after_setup_editor_handlers\n * @typedef {(() => void)[]} before_setup_editor_handlers\n *\n * @typedef {CSSSelector[]} o_editable_selectors\n */\n\nexport class SetupEditorPlugin extends Plugin {\n    static id = \"setup_editor_plugin\";\n    static shared = [\"getEditableAreas\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        closest_savable_providers: withSequence(10, (el) => el.closest(\".o_editable\")),\n        unremovable_node_predicates: (node) => node.classList?.contains(\"o_editable\"),\n    };\n\n    setup() {\n        const welcomeMessageEl = this.editable.querySelector(\n            \"#wrap .o_homepage_editor_welcome_message\"\n        );\n        welcomeMessageEl?.remove();\n        this.dispatchTo(\"before_setup_editor_handlers\");\n        let editableEls = this.getEditableElements(\n            this.getResource(\"o_editable_selectors\").join(\", \")\n        )\n            .filter((el) => !el.matches(\"link, script\"))\n            .filter((el) => !el.hasAttribute(\"data-oe-readonly\"))\n            .filter(\n                (el) =>\n                    !el.matches(\n                        'img[data-oe-field=\"arch\"], br[data-oe-field=\"arch\"], input[data-oe-field=\"arch\"]'\n                    )\n            )\n            .filter((el) => !el.classList.contains(\"oe_snippet_editor\"))\n            .filter((el) => !el.matches(\"hr, br, input, textarea\"))\n            .filter((el) => !el.hasAttribute(\"data-oe-sanitize-prevent-edition\"));\n        editableEls.concat(Array.from(this.editable.querySelectorAll(\".o_editable\")));\n        editableEls.forEach((el) => el.classList.add(\"o_editable\"));\n        if (this.delegateTo(\"after_setup_editor_handlers\")) {\n            return;\n        }\n\n        // Add automatic editor message on the editables where we can drag and\n        // drop elements.\n        editableEls = this.getEditableElements('.oe_structure.oe_empty, [data-oe-type=\"html\"]');\n        editableEls.forEach((el) => {\n            if (!el.hasAttribute(\"data-editor-message\")) {\n                el.setAttribute(\"data-editor-message-default\", true);\n                el.setAttribute(\"data-editor-message\", _t(\"DRAG BUILDING BLOCKS HERE\"));\n            }\n        });\n    }\n\n    getEditableElements(selector) {\n        const editableEls = [...this.editable.querySelectorAll(selector)]\n            .filter((el) => !el.matches(\".o_not_editable\"))\n            .filter((el) => {\n                const parent = el.closest(\".o_editable, .o_not_editable\");\n                return !parent || parent.matches(\".o_editable\");\n            });\n        return editableEls;\n    }\n\n    cleanForSave({ root }) {\n        root.classList.remove(\"o_editable\");\n        root.querySelectorAll(\".o_editable\").forEach((el) => {\n            el.classList.remove(\"o_editable\");\n        });\n\n        [root, ...root.querySelectorAll(\"[data-editor-message]\")].forEach((el) => {\n            el.removeAttribute(\"data-editor-message\");\n            el.removeAttribute(\"data-editor-message-default\");\n        });\n    }\n\n    /**\n     * Gets all the editable elements contained in the given root element (or\n     * the editable if none is specified), including this element.\n     *\n     * @param {HTMLElement|undefined} rootEl\n     * @returns {Array<HTMLElement}\n     */\n    getEditableAreas(rootEl) {\n        const editableEl = rootEl || this.editable;\n        const editablesAreaEls = [...editableEl.querySelectorAll(\".o_editable\")];\n        if (editableEl.matches(\".o_editable\")) {\n            editablesAreaEls.unshift(editableEl);\n        }\n        return editablesAreaEls;\n    }\n}\n", "import { isElement, isTextNode } from \"@html_editor/utils/dom_info\";\nimport {\n    Component,\n    onMounted,\n    onWillDestroy,\n    onWillStart,\n    onWillUpdateProps,\n    reactive,\n    toRaw,\n    useComponent,\n    useEffect,\n    useEnv,\n    useRef,\n    useState,\n    useSubEnv,\n} from \"@odoo/owl\";\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { effect } from \"@web/core/utils/reactive\";\nimport { useDebounced } from \"@web/core/utils/timing\";\n\n/**\n * @typedef { import(\"../../../../html_editor/static/src/editor\").EditorContext } EditorContext\n */\n\nfunction isConnectedElement(el) {\n    return el && el.isConnected && !!el.ownerDocument.defaultView;\n}\n\nexport function useDomState(getState, { checkEditingElement = true } = {}) {\n    const env = useEnv();\n    const isValid = (el) => (!el && !checkEditingElement) || isConnectedElement(el);\n    const handler = async (ev) => {\n        const editingElement = env.getEditingElement();\n        if (isValid(editingElement)) {\n            const newStatePromise = getState(editingElement);\n            if (ev) {\n                ev.detail.getStatePromises.push(newStatePromise);\n                const newState = await newStatePromise;\n                const shouldApply = await ev.detail.updatePromise;\n                if (shouldApply) {\n                    Object.assign(state, newState);\n                }\n            } else {\n                Object.assign(state, await newStatePromise);\n            }\n        }\n    };\n    const state = useState({});\n    onWillStart(handler);\n    useBus(env.editorBus, \"DOM_UPDATED\", handler);\n    return state;\n}\n\nexport function useActionInfo() {\n    const comp = useComponent();\n\n    const getParam = (paramName) => {\n        let param = comp.props[paramName];\n        param = param === undefined ? comp.env.weContext[paramName] : param;\n        if (typeof param === \"object\") {\n            param = JSON.stringify(param);\n        }\n        return param;\n    };\n\n    const actionParam = getParam(\"actionParam\");\n\n    return {\n        actionId: comp.props.action || comp.env.weContext.action,\n        actionParam,\n        actionValue: comp.props.actionValue,\n        classAction: getParam(\"classAction\"),\n        styleAction: getParam(\"styleAction\"),\n        styleActionValue: comp.props.styleActionValue,\n        attributeAction: getParam(\"attributeAction\"),\n        attributeActionValue: comp.props.attributeActionValue,\n    };\n}\n\nfunction querySelectorAll(targets, selector) {\n    const elements = new Set();\n    for (const target of targets) {\n        for (const el of target.querySelectorAll(selector)) {\n            elements.add(el);\n        }\n    }\n    return [...elements];\n}\n\nexport function useBuilderComponent() {\n    const comp = useComponent();\n    const newEnv = {};\n    const oldEnv = useEnv();\n    let editingElements;\n    let applyTo = comp.props.applyTo;\n    const updateEditingElements = () => {\n        editingElements = applyTo\n            ? querySelectorAll(oldEnv.getEditingElements(), applyTo)\n            : oldEnv.getEditingElements();\n    };\n    updateEditingElements();\n    oldEnv.editorBus.addEventListener(\"UPDATE_EDITING_ELEMENT\", updateEditingElements);\n    onWillUpdateProps(async (nextProps) => {\n        if (comp.props.applyTo !== nextProps.applyTo) {\n            applyTo = nextProps.applyTo;\n            oldEnv.editorBus.trigger(\"UPDATE_EDITING_ELEMENT\");\n            await oldEnv.triggerDomUpdated();\n        }\n    });\n    onWillDestroy(() => {\n        oldEnv.editorBus.removeEventListener(\"UPDATE_EDITING_ELEMENT\", updateEditingElements);\n    });\n    newEnv.getEditingElements = () => editingElements;\n    newEnv.getEditingElement = () => editingElements[0];\n    const weContext = {};\n    for (const key in basicContainerBuilderComponentProps) {\n        if (key in comp.props) {\n            weContext[key] = comp.props[key];\n        }\n    }\n    if (Object.keys(weContext).length) {\n        newEnv.weContext = { ...comp.env.weContext, ...weContext };\n    }\n    useSubEnv(newEnv);\n}\nexport function useDependencyDefinition(id, item, { onReady } = {}) {\n    const comp = useComponent();\n    const ignore = comp.env.ignoreBuilderItem;\n    if (onReady) {\n        onReady.then(() => {\n            comp.env.dependencyManager.add(id, item, ignore);\n        });\n    } else {\n        comp.env.dependencyManager.add(id, item, ignore);\n    }\n\n    onWillDestroy(() => {\n        comp.env.dependencyManager.removeByValue(item);\n    });\n}\n\nexport function useDependencies(dependencies) {\n    const env = useEnv();\n    const isDependenciesVisible = () => {\n        const deps = Array.isArray(dependencies) ? dependencies : [dependencies];\n        return deps.filter(Boolean).every((dependencyId) => {\n            const match = dependencyId.match(/(!)?(.*)/);\n            const inverse = !!match[1];\n            const id = match[2];\n            const isActiveFn = env.dependencyManager.get(id)?.isActive;\n            if (!isActiveFn) {\n                return false;\n            }\n            const isActive = isActiveFn();\n            return inverse ? !isActive : isActive;\n        });\n    };\n    return isDependenciesVisible;\n}\n\nfunction useIsActiveItem() {\n    const env = useEnv();\n    const listenedKeys = new Set();\n\n    function isActive(itemId) {\n        const isActiveFn = env.dependencyManager.get(itemId)?.isActive;\n        if (!isActiveFn) {\n            return false;\n        }\n        return isActiveFn();\n    }\n\n    const getState = () => {\n        const newState = {};\n        for (const itemId of listenedKeys) {\n            newState[itemId] = isActive(itemId);\n        }\n        return newState;\n    };\n    const state = useDomState(getState);\n    const listener = () => {\n        const newState = getState();\n        Object.assign(state, newState);\n    };\n    env.dependencyManager.addEventListener(\"dependency-updated\", listener);\n    onWillDestroy(() => {\n        env.dependencyManager.removeEventListener(\"dependency-updated\", listener);\n    });\n    return function isActiveItem(itemId) {\n        listenedKeys.add(itemId);\n        if (state[itemId] === undefined) {\n            return isActive(itemId);\n        }\n        return state[itemId];\n    };\n}\n\nexport function useGetItemValue() {\n    const env = useEnv();\n    const listenedKeys = new Set();\n\n    function getValue(itemId) {\n        const getValueFn = env.dependencyManager.get(itemId)?.getValue;\n        if (!getValueFn) {\n            return null;\n        }\n        return getValueFn();\n    }\n\n    const getState = () => {\n        const newState = {};\n        for (const itemId of listenedKeys) {\n            newState[itemId] = getValue(itemId);\n        }\n        return newState;\n    };\n    const state = useDomState(getState);\n    const listener = () => {\n        const newState = getState();\n        Object.assign(state, newState);\n    };\n    env.dependencyManager.addEventListener(\"dependency-updated\", listener);\n    onWillDestroy(() => {\n        env.dependencyManager.removeEventListener(\"dependency-updated\", listener);\n    });\n    return function getItemValue(itemId) {\n        listenedKeys.add(itemId);\n        if (!(itemId in state)) {\n            return getValue(itemId);\n        }\n        return state[itemId];\n    };\n}\n\nexport function useSelectableComponent(id, { onItemChange } = {}) {\n    useBuilderComponent();\n    const selectableItems = [];\n    const refreshCurrentItemDebounced = useDebounced(refreshCurrentItem, 0, { immediate: true });\n    const env = useEnv();\n\n    const state = reactive({\n        currentSelectedItem: null,\n    });\n\n    function refreshCurrentItem() {\n        if (env.editor.isDestroyed || env.editor.shared.history.getIsPreviewing()) {\n            return;\n        }\n        let currentItem;\n        let itemPriority = 0;\n        for (const selectableItem of selectableItems) {\n            if (selectableItem.isApplied() && selectableItem.priority >= itemPriority) {\n                currentItem = selectableItem;\n                itemPriority = selectableItem.priority;\n            }\n        }\n        if (currentItem && currentItem !== toRaw(state.currentSelectedItem)) {\n            state.currentSelectedItem = currentItem;\n            env.dependencyManager.triggerDependencyUpdated();\n        }\n        if (currentItem) {\n            onItemChange?.(currentItem);\n        }\n    }\n\n    if (id) {\n        useDependencyDefinition(id, {\n            type: \"select\",\n            getSelectableItems: () => selectableItems.slice(0),\n        });\n    }\n\n    onMounted(refreshCurrentItem);\n    useBus(env.editorBus, \"DOM_UPDATED\", refreshCurrentItem);\n    function cleanSelectedItem(...args) {\n        if (state.currentSelectedItem) {\n            return state.currentSelectedItem.clean(...args);\n        }\n    }\n\n    useSubEnv({\n        selectableContext: {\n            cleanSelectedItem,\n            addSelectableItem: (item) => {\n                selectableItems.push(item);\n            },\n            removeSelectableItem: (item) => {\n                const index = selectableItems.indexOf(item);\n                if (index !== -1) {\n                    selectableItems.splice(index, 1);\n                }\n            },\n            update: refreshCurrentItemDebounced,\n            items: selectableItems,\n            refreshCurrentItem: () => refreshCurrentItem(),\n            getSelectableState: () => state,\n        },\n    });\n}\n\nexport function useSelectableItemComponent(id, { getLabel = () => {} } = {}) {\n    const { operation, isApplied, getActions, priority, clean, onReady } =\n        useClickableBuilderComponent();\n    const env = useEnv();\n\n    let isSelectableActive = isApplied;\n    let state;\n    if (env.selectableContext) {\n        const selectableState = env.selectableContext.getSelectableState();\n        isSelectableActive = () => {\n            env.selectableContext.refreshCurrentItem();\n            return (\n                toRaw(selectableState.currentSelectedItem) === selectableItem ||\n                (id && selectableState.currentSelectedItem?.id === id)\n            );\n        };\n\n        const selectableItem = {\n            isApplied,\n            priority,\n            getLabel,\n            clean,\n            getActions,\n            id,\n        };\n\n        env.selectableContext.addSelectableItem(selectableItem);\n        state = useState({\n            isActive: false,\n        });\n        effect(\n            ({ currentSelectedItem }) => {\n                state.isActive =\n                    toRaw(currentSelectedItem) === selectableItem ||\n                    (id && currentSelectedItem?.id === id);\n            },\n            [selectableState]\n        );\n        env.selectableContext.refreshCurrentItem();\n        onMounted(env.selectableContext.update);\n        onWillDestroy(() => {\n            env.selectableContext.removeSelectableItem(selectableItem);\n        });\n    } else {\n        state = useDomState(async () => {\n            await onReady;\n            return {\n                isActive: isSelectableActive(),\n            };\n        });\n    }\n\n    if (id) {\n        useDependencyDefinition(\n            id,\n            {\n                isActive: isSelectableActive,\n                getActions,\n                cleanSelectedItem: env.selectableContext?.cleanSelectedItem,\n            },\n            { onReady }\n        );\n    }\n\n    return { state, operation };\n}\n\nfunction usePrepareAction(getAllActions) {\n    const env = useEnv();\n    const getAction = env.editor.shared.builderActions.getAction;\n    const asyncActions = [];\n    for (const descr of getAllActions()) {\n        if (descr.actionId) {\n            const action = getAction(descr.actionId);\n            if (action.has(\"prepare\")) {\n                asyncActions.push({ action, descr });\n            }\n        }\n    }\n    let onReady;\n    if (asyncActions.length) {\n        let resolve;\n        onReady = new Promise((r) => {\n            resolve = r;\n        });\n        onWillStart(async function () {\n            await Promise.all(asyncActions.map((obj) => obj.action.prepare(obj.descr)));\n            resolve();\n        });\n        onWillUpdateProps(async ({ actionParam, actionValue }) => {\n            onReady = new Promise((r) => {\n                resolve = r;\n            });\n            // TODO: should we support updating actionId?\n            await Promise.all(\n                asyncActions.map((obj) =>\n                    obj.action.prepare({\n                        ...obj.descr,\n                        actionParam: convertParamToObject(actionParam),\n                        actionValue,\n                    })\n                )\n            );\n            resolve();\n        });\n    }\n    return onReady;\n}\n\nfunction useReloadAction(getAllActions) {\n    const env = useEnv();\n    const getAction = env.editor.shared.builderActions.getAction;\n    let reload = false;\n    for (const descr of getAllActions()) {\n        if (descr.actionId) {\n            const action = getAction(descr.actionId);\n            if (action.reload) {\n                reload = action.reload;\n            }\n        }\n    }\n    return { reload };\n}\n\nexport function useHasPreview(getAllActions) {\n    const comp = useComponent();\n    const getAction = comp.env.editor.shared.builderActions.getAction;\n\n    let hasPreview = true;\n    for (const descr of getAllActions()) {\n        if (descr.actionId) {\n            const action = getAction(descr.actionId);\n            if (action.preview === false) {\n                hasPreview = false;\n            }\n        }\n    }\n\n    return (\n        hasPreview &&\n        (comp.props.preview === true ||\n            (comp.props.preview === undefined && comp.env.weContext.preview !== false))\n    );\n}\n\nfunction useWithLoadingEffect(getAllActions) {\n    const env = useEnv();\n    const getAction = env.editor.shared.builderActions.getAction;\n    let withLoadingEffect = true;\n    for (const descr of getAllActions()) {\n        if (descr.actionId) {\n            const action = getAction(descr.actionId);\n            if (action.withLoadingEffect === false) {\n                withLoadingEffect = false;\n            }\n        }\n    }\n\n    return withLoadingEffect;\n}\n\nexport function revertPreview(editor) {\n    if (editor.isDestroyed) {\n        return;\n    }\n    // The `next` will cancel the previous operation, which will revert\n    // the operation in case of a preview.\n    return editor.shared.operation.next();\n}\n\nexport function useClickableBuilderComponent() {\n    useBuilderComponent();\n    const comp = useComponent();\n    const { getAllActions, callOperation, isApplied } = getAllActionsAndOperations(comp);\n    const getAction = comp.env.editor.shared.builderActions.getAction;\n\n    const onReady = usePrepareAction(getAllActions);\n    const { reload } = useReloadAction(getAllActions);\n\n    const applyOperation = comp.env.editor.shared.history.makePreviewableAsyncOperation(callApply);\n    const inheritedActionIds =\n        comp.props.inheritedActions || comp.env.weContext.inheritedActions || [];\n\n    const hasPreview = useHasPreview(getAllActions);\n    const operationWithReload = useOperationWithReload(callApply, reload);\n\n    const withLoadingEffect = useWithLoadingEffect(getAllActions);\n\n    let preventNextPreview = false;\n    const operation = {\n        commit: () => {\n            preventNextPreview = false;\n            if (reload) {\n                callOperation(operationWithReload);\n            } else {\n                callOperation(applyOperation.commit, {\n                    operationParams: {\n                        withLoadingEffect: withLoadingEffect,\n                    },\n                });\n            }\n        },\n        preview: () => {\n            // Avoid previewing the same option twice.\n            if (preventNextPreview) {\n                return;\n            }\n            preventNextPreview = true;\n            callOperation(applyOperation.preview, {\n                preview: true,\n                operationParams: {\n                    cancellable: true,\n                    cancelPrevious: () => applyOperation.revert(),\n                },\n            });\n        },\n        revert: () => {\n            preventNextPreview = false;\n            revertPreview(comp.env.editor);\n        },\n    };\n\n    if (!hasPreview) {\n        operation.preview = () => {};\n    }\n\n    function clean(nextApplySpecs, isPreviewing) {\n        const proms = [];\n        for (const { actionId, actionParam, actionValue } of getAllActions()) {\n            for (const editingElement of comp.env.getEditingElements()) {\n                let nextAction;\n                proms.push(\n                    getAction(actionId).clean?.({\n                        isPreviewing,\n                        editingElement,\n                        params: actionParam,\n                        value: actionValue,\n                        dependencyManager: comp.env.dependencyManager,\n                        selectableContext: comp.env.selectableContext,\n                        get nextAction() {\n                            nextAction =\n                                nextAction ||\n                                nextApplySpecs.find((a) => a.actionId === actionId) ||\n                                {};\n                            return {\n                                params: nextAction.actionParam,\n                                value: nextAction.actionValue,\n                            };\n                        },\n                    })\n                );\n            }\n        }\n        return Promise.all(proms);\n    }\n\n    async function callApply(applySpecs, isPreviewing) {\n        await comp.env.selectableContext?.cleanSelectedItem(applySpecs, isPreviewing);\n        const cleans = inheritedActionIds\n            .map((actionId) => comp.env.dependencyManager.get(actionId).cleanSelectedItem)\n            .filter(Boolean);\n        const cleanPromises = [];\n        for (const clean of new Set(cleans)) {\n            cleanPromises.push(clean(applySpecs, isPreviewing));\n        }\n        await Promise.all(cleanPromises);\n        const cleanOrApplyProms = [];\n        const isAlreadyApplied = isApplied();\n        for (const applySpec of applySpecs) {\n            const hasClean = !!applySpec.clean;\n            const shouldClean = _shouldClean(comp, hasClean, isAlreadyApplied);\n            if (shouldClean) {\n                cleanOrApplyProms.push(\n                    applySpec.action.clean({\n                        isPreviewing,\n                        editingElement: applySpec.editingElement,\n                        params: applySpec.actionParam,\n                        value: applySpec.actionValue,\n                        loadResult: applySpec.loadOnClean ? applySpec.loadResult : null,\n                        dependencyManager: comp.env.dependencyManager,\n                        selectableContext: comp.env.selectableContext,\n                    })\n                );\n            } else {\n                cleanOrApplyProms.push(\n                    applySpec.action.apply({\n                        isPreviewing,\n                        editingElement: applySpec.editingElement,\n                        params: applySpec.actionParam,\n                        value: applySpec.actionValue,\n                        loadResult: applySpec.loadResult,\n                        dependencyManager: comp.env.dependencyManager,\n                        selectableContext: comp.env.selectableContext,\n                    })\n                );\n            }\n        }\n        await Promise.all(cleanOrApplyProms);\n    }\n    function getPriority() {\n        return (\n            getAllActions()\n                .map(\n                    (a) =>\n                        getAction(a.actionId).getPriority?.({\n                            params: a.actionParam,\n                            value: a.actionValue,\n                        }) || 0\n                )\n                .find((x) => x !== 0) || 0\n        );\n    }\n\n    return {\n        operation,\n        isApplied,\n        clean,\n        priority: getPriority(),\n        getActions: getAllActions,\n        onReady,\n    };\n}\nfunction useOperationWithReload(callApply, reload) {\n    const env = useEnv();\n    return async (...args) => {\n        const { editingElement } = args[0][0];\n        await callApply(...args);\n        env.editor.shared.history.addStep();\n        await env.editor.shared.savePlugin.save();\n        const target = env.editor.shared.builderOptions.getReloadSelector(editingElement);\n        const url = reload.getReloadUrl?.();\n        await env.editor.config.reloadEditor({ target, url });\n    };\n}\n\nfunction getValueWithDefault(userInputValue, defaultValue, formatRawValue) {\n    if (defaultValue !== undefined) {\n        if (!userInputValue || (typeof userInputValue === \"string\" && !userInputValue.trim())) {\n            return formatRawValue(defaultValue);\n        }\n    }\n    return userInputValue;\n}\n\nexport function useInputBuilderComponent({\n    id,\n    defaultValue,\n    formatRawValue = (rawValue) => rawValue,\n    parseDisplayValue = (displayValue) => displayValue,\n} = {}) {\n    const comp = useComponent();\n    const { getAllActions, callOperation } = getAllActionsAndOperations(comp);\n    const getAction = comp.env.editor.shared.builderActions.getAction;\n    const state = useDomState(getState);\n\n    const onReady = usePrepareAction(getAllActions);\n    const { reload } = useReloadAction(getAllActions);\n\n    const withLoadingEffect = useWithLoadingEffect(getAllActions);\n\n    onWillUpdateProps((nextProps) => {\n        if (\"default\" in nextProps) {\n            defaultValue = nextProps.default;\n        }\n    });\n\n    async function callApply(applySpecs, isPreviewing) {\n        const proms = [];\n        for (const applySpec of applySpecs) {\n            proms.push(\n                applySpec.action.apply({\n                    isPreviewing,\n                    editingElement: applySpec.editingElement,\n                    params: applySpec.actionParam,\n                    value: applySpec.actionValue,\n                    loadResult: applySpec.loadResult,\n                    dependencyManager: comp.env.dependencyManager,\n                })\n            );\n        }\n        await Promise.all(proms);\n    }\n\n    const applyOperation = comp.env.editor.shared.history.makePreviewableAsyncOperation(callApply);\n    const operationWithReload = useOperationWithReload(callApply, reload);\n    function getState(editingElement) {\n        if (!isConnectedElement(editingElement)) {\n            // TODO try to remove it. We need to move hook in BuilderComponent\n            return {};\n        }\n        const actionWithGetValue = getAllActions().find(\n            ({ actionId }) => getAction(actionId).getValue\n        );\n        const { actionId, actionParam } = actionWithGetValue;\n        const actionValue =\n            getAction(actionId).getValue({ editingElement, params: actionParam }) || defaultValue;\n        return {\n            value: actionValue,\n        };\n    }\n\n    function commit(userInputValue) {\n        userInputValue = getValueWithDefault(userInputValue, defaultValue, formatRawValue);\n        const rawValue = parseDisplayValue(userInputValue);\n        if (reload) {\n            callOperation(operationWithReload, { userInputValue: rawValue });\n        } else {\n            callOperation(applyOperation.commit, {\n                userInputValue: rawValue,\n                withLoadingEffect: withLoadingEffect,\n            });\n        }\n        if (rawValue === null || (rawValue === defaultValue && rawValue === state.value)) {\n            state.value = rawValue;\n        }\n        // If the parsed value is not equivalent to the user input, we want to\n        // normalize the displayed value. It is useful in cases of invalid\n        // input and allows to fall back to the output of parseDisplayValue.\n        return rawValue !== undefined ? formatRawValue(rawValue) : \"\";\n    }\n\n    const shouldPreview = useHasPreview(getAllActions);\n    function preview(userInputValue) {\n        if (shouldPreview) {\n            userInputValue = getValueWithDefault(userInputValue, defaultValue, formatRawValue);\n            callOperation(applyOperation.preview, {\n                preview: true,\n                userInputValue: parseDisplayValue(userInputValue),\n                operationParams: {\n                    cancellable: true,\n                    cancelPrevious: () => applyOperation.revert(),\n                },\n            });\n        }\n    }\n\n    if (id) {\n        useDependencyDefinition(\n            id,\n            {\n                type: \"input\",\n                getValue: () => state.value,\n            },\n            { onReady }\n        );\n    }\n\n    return {\n        state,\n        commit,\n        preview,\n        onReady,\n    };\n}\n\nexport function useApplyVisibility(refName) {\n    const ref = useRef(refName);\n    return (hasContent) => {\n        ref.el?.classList.toggle(\"d-none\", !hasContent);\n    };\n}\n\nexport function useVisibilityObserver(contentName, callback) {\n    const contentRef = useRef(contentName);\n\n    const applyVisibility = () => {\n        const hasContent = [...contentRef.el.childNodes].some(\n            (el) =>\n                (isTextNode(el) && el.textContent !== \"\") ||\n                (isElement(el) && !el.classList.contains(\"d-none\"))\n        );\n        callback(hasContent);\n    };\n\n    const observer = new MutationObserver(applyVisibility);\n    useEffect(\n        (contentEl) => {\n            if (!contentEl) {\n                return;\n            }\n            applyVisibility();\n            observer.observe(contentEl, {\n                subtree: true,\n                attributes: true,\n                childList: true,\n                attributeFilter: [\"class\"],\n            });\n            return () => {\n                observer.disconnect();\n            };\n        },\n        () => [contentRef.el]\n    );\n}\n\nexport function useInputDebouncedCommit(ref) {\n    const comp = useComponent();\n    return useDebounced(() => {\n        const normalizedDisplayValue = comp.commit(ref.el.value);\n        ref.el.value = normalizedDisplayValue;\n    }, 550);\n    // \u2191 500 is the delay when holding keydown between the 1st and 2nd event\n    // fired. Some additional delay by the browser may add another ~5-10ms.\n    // We debounce above that threshold to keep a single history step when\n    // holding up/down on a number or range input.\n}\n\nexport const basicContainerBuilderComponentProps = {\n    id: { type: String, optional: true },\n    applyTo: { type: String, optional: true },\n    preview: { type: Boolean, optional: true },\n    inheritedActions: { type: Array, element: String, optional: true },\n    // preview: { type: Boolean, optional: true },\n    // reloadPage: { type: Boolean, optional: true },\n\n    action: { type: String, optional: true },\n    actionParam: { validate: () => true, optional: true },\n\n    // Shorthand actions.\n    classAction: { validate: () => true, optional: true },\n    attributeAction: { validate: () => true, optional: true },\n    dataAttributeAction: { validate: () => true, optional: true },\n    styleAction: { validate: () => true, optional: true },\n};\nconst validateIsNull = { validate: (value) => value === null };\n\nexport const clickableBuilderComponentProps = {\n    ...basicContainerBuilderComponentProps,\n    inverseAction: { type: Boolean, optional: true },\n\n    actionValue: {\n        type: [Boolean, String, Number, { type: Array, element: [Boolean, String, Number] }],\n        optional: true,\n    },\n\n    // Shorthand actions values.\n    classActionValue: { type: [String, Array, validateIsNull], optional: true },\n    attributeActionValue: { type: [String, Array, validateIsNull], optional: true },\n    dataAttributeActionValue: { type: [String, Array, validateIsNull], optional: true },\n    styleActionValue: { type: [String, Array, validateIsNull], optional: true },\n\n    inheritedActions: { type: Array, element: String, optional: true },\n};\n\nexport function getAllActionsAndOperations(comp) {\n    const inheritedActionIds =\n        comp.props.inheritedActions || comp.env.weContext.inheritedActions || [];\n\n    function getActionsSpecs(actions, userInputValue) {\n        const getAction = comp.env.editor.shared.builderActions.getAction;\n        const overridableMethods = [\"apply\", \"clean\", \"load\", \"loadOnClean\"];\n        const specs = [];\n        for (let { actionId, actionParam, actionValue } of actions) {\n            const action = getAction(actionId);\n            // Take the action value defined by the clickable or the input given\n            // by the user.\n            actionValue = actionValue === undefined ? userInputValue : actionValue;\n            for (const editingElement of comp.env.getEditingElements()) {\n                const spec = {\n                    editingElement,\n                    actionId,\n                    actionParam,\n                    actionValue,\n                    action,\n                };\n                // TODO Since the action is now in the spec, this shouldn't be\n                // necessary anymore.\n                for (const method of overridableMethods) {\n                    if (!action.has || action.has(method)) {\n                        spec[method] = action[method];\n                    }\n                }\n                specs.push(spec);\n            }\n        }\n        return specs;\n    }\n    function getShorthandActions() {\n        const actions = [];\n        const shorthands = [\n            [\"classAction\", \"classActionValue\"],\n            [\"attributeAction\", \"attributeActionValue\"],\n            [\"dataAttributeAction\", \"dataAttributeActionValue\"],\n            [\"styleAction\", \"styleActionValue\"],\n        ];\n        for (const [actionId, actionValue] of shorthands) {\n            const actionParam = comp.env.weContext[actionId] || comp.props[actionId];\n            if (actionParam !== undefined) {\n                actions.push({\n                    actionId,\n                    actionParam: convertParamToObject(actionParam),\n                    actionValue: comp.props[actionValue],\n                });\n            }\n        }\n        return actions;\n    }\n    function getCustomAction() {\n        const actionId = comp.props.action || comp.env.weContext.action;\n        if (actionId) {\n            const actionParam = comp.props.actionParam ?? comp.env.weContext.actionParam;\n            return {\n                actionId: actionId,\n                actionParam: convertParamToObject(actionParam),\n                actionValue: comp.props.actionValue,\n            };\n        }\n    }\n    function getAllActions() {\n        const actions = getShorthandActions();\n\n        const { actionId, actionParam, actionValue } = getCustomAction() || {};\n        if (actionId) {\n            actions.push({ actionId, actionParam, actionValue });\n        }\n        const inheritedActions =\n            inheritedActionIds\n                .map(\n                    (actionId) =>\n                        comp.env.dependencyManager\n                            // The dependency might not be loaded yet.\n                            .get(actionId)\n                            ?.getActions?.() || []\n                )\n                .flat() || [];\n        return actions.concat(inheritedActions || []);\n    }\n    function callOperation(fn, params = {}) {\n        const isPreviewing = !!params.preview;\n        const actionsSpecs = getActionsSpecs(getAllActions(), params.userInputValue);\n\n        comp.env.editor.shared.operation.next(() => fn(actionsSpecs, isPreviewing), {\n            load: async () =>\n                Promise.all(\n                    actionsSpecs.map(async (applySpec) => {\n                        if (!applySpec.action.has(\"load\")) {\n                            return;\n                        }\n                        const hasClean = !!applySpec.action.has(\"clean\");\n                        if (!applySpec.loadOnClean && _shouldClean(comp, hasClean, isApplied())) {\n                            // The element will be cleaned, do not load\n                            return;\n                        }\n                        const result = await applySpec.action.load({\n                            editingElement: applySpec.editingElement,\n                            params: applySpec.actionParam,\n                            value: applySpec.actionValue,\n                        });\n                        applySpec.loadResult = result;\n                    })\n                ),\n            ...params.operationParams,\n        });\n    }\n    function isApplied() {\n        const getAction = comp.env.editor.shared.builderActions.getAction;\n        const editingElements = comp.env.getEditingElements();\n        if (!editingElements.length) {\n            return;\n        }\n        const areActionsActiveTabs = getAllActions().map((o) => {\n            const { actionId, actionParam, actionValue } = o;\n            // TODO isApplied === first editing el or all ?\n            const editingElement = editingElements[0];\n            if (!isConnectedElement(editingElement)) {\n                return false;\n            }\n            const isApplied = getAction(actionId).isApplied?.({\n                editingElement,\n                params: actionParam,\n                value: actionValue,\n            });\n            return comp.props.inverseAction ? !isApplied : isApplied;\n        });\n        // If there is no `isApplied` method for the widget return false\n        if (areActionsActiveTabs.every((el) => el === undefined)) {\n            return false;\n        }\n        // If `isApplied` is explicitly false for an action return false\n        if (areActionsActiveTabs.some((el) => el === false)) {\n            return false;\n        }\n        // `isApplied` is true for at least one action\n        return true;\n    }\n    return {\n        getAllActions: getAllActions,\n        callOperation: callOperation,\n        isApplied: isApplied,\n    };\n}\nfunction _shouldClean(comp, hasClean, isApplied) {\n    if (!hasClean) {\n        return false;\n    }\n    const shouldToggle = !comp.env.selectableContext;\n    const shouldClean = shouldToggle && isApplied;\n    return comp.props.inverseAction ? !shouldClean : shouldClean;\n}\nexport function convertParamToObject(param) {\n    if (param === undefined) {\n        param = {};\n    } else if (param instanceof Array || param instanceof Function || !(param instanceof Object)) {\n        param = {\n            [\"mainParam\"]: param,\n        };\n    }\n    return param;\n}\n\nexport class BaseOptionComponent extends Component {\n    static components = {};\n    static props = {};\n    static template = \"\";\n\n    setup() {\n        /** @type {EditorContext} */\n        const context = this.env.editor.shared.builderOptions.getBuilderOptionContext(\n            this.constructor\n        );\n        /** @type { EditorContext['document'] } **/\n        this.document = context.document;\n        this.window = context.document.defaultView;\n        /** @type { EditorContext['editable'] } **/\n        this.editable = context.editable;\n        /** @type { EditorContext['config'] } **/\n        this.config = context.config;\n        /** @type { EditorContext['services'] } **/\n        this.services = context.services;\n        /** @type { EditorContext['dependencies'] } **/\n        this.dependencies = context.dependencies;\n        /** @type { EditorContext['getResource'] } **/\n        this.getResource = context.getResource;\n        /** @type { EditorContext['dispatchTo'] } **/\n        this.dispatchTo = context.dispatchTo;\n        /** @type { EditorContext['delegateTo'] } **/\n        this.delegateTo = context.delegateTo;\n\n        this.isActiveItem = useIsActiveItem();\n        const comp = useComponent();\n        const editor = comp.env.editor;\n\n        if (!comp.constructor.components) {\n            comp.constructor.components = {};\n        }\n        const Components = editor.shared.builderComponents.getComponents();\n        Object.assign(comp.constructor.components, Components);\n    }\n    /**\n     * Check if the given items are active.\n     *\n     * Map over all items to listen for any reactive value changes.\n     *\n     * @param {string[]} itemIds - The IDs of the items to check.\n     * @returns {boolean} - True if the item is active, false otherwise.\n     */\n    isActiveItems(itemIds) {\n        return itemIds.map((i) => this.isActiveItem(i)).find(Boolean) || false;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { getElementsWithOption } from \"@html_builder/utils/utils\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\n/**\n * @typedef { Object } VisibilityShared\n * @property { VisibilityPlugin['getVisibleSibling'] } getVisibleSibling\n * @property { VisibilityPlugin['toggleTargetVisibility'] } toggleTargetVisibility\n * @property { VisibilityPlugin['onOptionVisibilityUpdate'] } onOptionVisibilityUpdate\n * @property { VisibilityPlugin['hideElement'] } hideElement\n */\n\n/**\n * @typedef {((targetEl: HTMLElement) => void)[]} target_hide\n * @typedef {((targetEl: HTMLElement) => void)[]} target_show\n */\n\nconst invisibleElementsSelector =\n    \".o_snippet_invisible, .o_snippet_mobile_invisible, .o_snippet_desktop_invisible\";\nconst deviceInvisibleSelector = \".o_snippet_mobile_invisible, .o_snippet_desktop_invisible\";\n\nexport class VisibilityPlugin extends Plugin {\n    static id = \"visibility\";\n    static dependencies = [\"builderOptions\", \"disableSnippets\", \"history\"];\n    static shared = [\n        \"getVisibleSibling\",\n        \"toggleTargetVisibility\",\n        \"onOptionVisibilityUpdate\",\n        \"hideElement\",\n    ];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        on_mobile_preview_clicked: withSequence(10, this.onMobilePreviewClicked.bind(this)),\n        system_attributes: [\"data-invisible\"],\n        system_classes: [\"o_snippet_override_invisible\"],\n        clean_for_save_handlers: this.cleanForSaveVisibility.bind(this),\n        on_snippet_dropped_handlers: this.onSnippetDropped.bind(this),\n        on_restore_containers_handlers: (newTargetEl) => this.makeTargetVisible(newTargetEl),\n    };\n\n    setup() {\n        // Add the `data-invisible=\"1\"` attribute on the elements that are\n        // really hidden, and remove it from the ones that are in fact visible,\n        // depending on if we are in mobile preview or not, so the DOM is\n        // consistent.\n        const isMobilePreview = this.config.isMobileView(this.editable);\n        this.editable.querySelectorAll(deviceInvisibleSelector).forEach((invisibleEl) => {\n            const isMobileHidden = invisibleEl.matches(\".o_snippet_mobile_invisible\");\n            const isDesktopHidden = invisibleEl.matches(\".o_snippet_desktop_invisible\");\n            if ((isMobileHidden && isMobilePreview) || (isDesktopHidden && !isMobilePreview)) {\n                invisibleEl.setAttribute(\"data-invisible\", \"1\");\n            } else {\n                invisibleEl.removeAttribute(\"data-invisible\");\n            }\n        });\n    }\n\n    getVisibleSibling(target, direction) {\n        const systemNodeSelectors = this.getResource(\"system_node_selectors\").join(\",\");\n        const siblingEls = [...target.parentNode.children];\n        const visibleSiblingEls = siblingEls.filter(\n            (el) =>\n                !el.classList.contains(\"o_we_no_overlay\") &&\n                window.getComputedStyle(el).display !== \"none\" &&\n                !el.closest(systemNodeSelectors)\n        );\n        const targetMobileOrder = target.style.order;\n        // On mobile, if the target has a mobile order (which is independent\n        // from desktop), consider these orders instead of the DOM order.\n        if (targetMobileOrder && this.config.isMobileView(target)) {\n            visibleSiblingEls.sort((a, b) => parseInt(a.style.order) - parseInt(b.style.order));\n        }\n        const targetIndex = visibleSiblingEls.indexOf(target);\n        const siblingIndex = direction === \"prev\" ? targetIndex - 1 : targetIndex + 1;\n        if (siblingIndex === -1 || siblingIndex === visibleSiblingEls.length) {\n            return false;\n        }\n        return visibleSiblingEls[siblingIndex];\n    }\n\n    /**\n     * Toggles the visibility of the given element and its ancestors if it was\n     * not visible.\n     *\n     * @param {HTMLElement} targetEl the element\n     */\n    makeTargetVisible(targetEl) {\n        let invisibleEl = targetEl.closest(\"[data-invisible='1']\");\n        if (!invisibleEl) {\n            return;\n        }\n        while (invisibleEl) {\n            this.toggleTargetVisibility(invisibleEl, true);\n            invisibleEl = targetEl.closest(\"[data-invisible='1']\");\n        }\n        this.config.updateInvisibleElementsPanel();\n    }\n\n    cleanForSaveVisibility({ root: rootEl }) {\n        const invisibleEls = getElementsWithOption(rootEl, invisibleElementsSelector);\n        for (const invisibleEl of invisibleEls) {\n            // Hide the invisible elements.\n            this.toggleTargetVisibility(invisibleEl, false, false, true);\n            // Remove the `data-invisible` attribute from conditionally hidden\n            // elements.\n            if (invisibleEl.matches(\"[data-visibility='conditional']\")) {\n                invisibleEl.removeAttribute(\"data-invisible\");\n            }\n        }\n    }\n\n    onSnippetDropped({ snippetEl }) {\n        // Show the invisible elements.\n        const invisibleEls = getElementsWithOption(snippetEl, invisibleElementsSelector);\n        for (const invisibleEl of invisibleEls) {\n            this.toggleTargetVisibility(invisibleEl, true);\n        }\n    }\n\n    onMobilePreviewClicked() {\n        const deviceInvisibleEls = this.editable.querySelectorAll(deviceInvisibleSelector);\n        const currentContainerTargetEl = this.dependencies[\"builderOptions\"].getTarget();\n        for (const deviceInvisibleEl of [...deviceInvisibleEls]) {\n            deviceInvisibleEl.classList.remove(\"o_snippet_override_invisible\");\n            const show = !isTargetVisible(deviceInvisibleEl);\n            const isShown = this.toggleVisibilityStatus(deviceInvisibleEl, show, true);\n            if (!isShown && deviceInvisibleEl.contains(currentContainerTargetEl)) {\n                this.dependencies.builderOptions.deactivateContainers();\n            }\n        }\n    }\n\n    /**\n     * Toggles the visibility of the given element.\n     *\n     * @param {HTMLElement} editingEl\n     * @param {Boolean} show true to show the element, false to hide it\n     * @param {Boolean} considerDeviceVisibility\n     * @param {Boolean} [isCleaning=false] true if the function is called by the\n     * clean_for_save handler.\n     * @returns {Boolean}\n     */\n    toggleTargetVisibility(editingEl, show, considerDeviceVisibility, isCleaning = false) {\n        show = this.toggleVisibilityStatus(editingEl, show, considerDeviceVisibility);\n        const resourceName = show ? \"target_show\" : \"target_hide\";\n        this.dispatchTo(resourceName, editingEl);\n        return show;\n    }\n\n    /**\n     * Called when an option changed the visibility of its editing element.\n     *\n     * @param {HTMLElement} editingEl the editing element\n     * @param {Boolean} show true/false if the element was shown/hidden\n     */\n    onOptionVisibilityUpdate(editingEl, show) {\n        if (this.dependencies.history.getIsPreviewing()) {\n            return;\n        }\n        const isShown = this.toggleVisibilityStatus(editingEl, show);\n        if (!isShown) {\n            this.dependencies.builderOptions.setNextTarget(false);\n        }\n        this.config.updateInvisibleElementsPanel();\n        this.dependencies.disableSnippets.disableUndroppableSnippets();\n    }\n\n    /**\n     * Sets/removes the `data-invisible` attribute on the given element,\n     * depending on if it is considered as hidden/shown.\n     *\n     * @param {HTMLElement} editingEl the element\n     * @param {Boolean} show\n     * @param {Boolean} considerDeviceVisibility\n     * @returns {Boolean}\n     */\n    toggleVisibilityStatus(editingEl, show, considerDeviceVisibility = false) {\n        if (\n            considerDeviceVisibility &&\n            editingEl.matches(\".o_snippet_mobile_invisible, .o_snippet_desktop_invisible\")\n        ) {\n            const isMobilePreview = this.config.isMobileView(editingEl);\n            const isMobileHidden = editingEl.classList.contains(\"o_snippet_mobile_invisible\");\n            show = isMobilePreview !== isMobileHidden;\n        }\n\n        if (show === undefined) {\n            show = !isTargetVisible(editingEl);\n        }\n        if (show) {\n            delete editingEl.dataset.invisible;\n        } else {\n            editingEl.dataset.invisible = \"1\";\n        }\n        return show;\n    }\n\n    /**\n     * Hides the given element and updates what needs to be.\n     * Note: to use only when hiding things without adding history steps:\n     * - if an action adding a history step hides the element, it should call\n     *   `onOptionVisibilityUpdate`\n     * - if it concerns the \"Invisible Element\" panel, refer to its component.\n     *\n     * @param {HTMLElement} toHideEl the element to hide.\n     */\n    hideElement(toHideEl) {\n        this.toggleTargetVisibility(toHideEl, false);\n        this.dependencies.builderOptions.deactivateContainers();\n        this.config.updateInvisibleElementsPanel();\n        this.dependencies.disableSnippets.disableUndroppableSnippets();\n    }\n}\n\nfunction isTargetVisible(editingEl) {\n    return editingEl.dataset.invisible !== \"1\";\n}\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class BaseAddProductOption extends BaseOptionComponent {\n    static template = \"html_builder.AddProductOption\";\n}\n", "import { before, WIDTH } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { fonts } from \"@html_editor/utils/fonts\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nclass AlertOptionPlugin extends Plugin {\n    static id = \"alertOption\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_actions: {\n            AlertIconAction,\n        },\n        builder_options: [withSequence(before(WIDTH), AlertOption)],\n        so_content_addition_selector: [\".s_alert\"],\n    };\n}\n\nexport class AlertOption extends BaseOptionComponent {\n    static template = \"html_builder.AlertOption\";\n    static selector = \".s_alert\";\n    static name = \"alertTypeOption\";\n}\n\nexport class AlertIconAction extends BuilderAction {\n    static id = \"alertIcon\";\n    apply({ editingElement, params: { mainParam: className } }) {\n        const icon = editingElement.querySelector(\".s_alert_icon\");\n        if (!icon) {\n            return;\n        }\n        fonts.computeFonts();\n        const allFaIcons = fonts.fontIcons[0].alias;\n        icon.classList.remove(...allFaIcons);\n        icon.classList.add(className);\n    }\n    clean({ editingElement, params: { mainParam: className } }) {\n        const icon = editingElement.querySelector(\".s_alert_icon\");\n        if (!icon) {\n            return;\n        }\n        icon.classList.remove(className);\n    }\n    isApplied({ editingElement, params: { mainParam: className } }) {\n        const iconEl = editingElement.querySelector(\".s_alert_icon\");\n        if (!iconEl) {\n            return;\n        }\n        return iconEl.classList.contains(className);\n    }\n}\nregistry.category(\"builder-plugins\").add(AlertOptionPlugin.id, AlertOptionPlugin);\n", "export function useBackgroundOption(isActiveItem) {\n    return { showColorFilter: () => isActiveItem(\"toggle_bg_image_id\") };\n}\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { getBgImageURLFromEl, normalizeColor } from \"@html_builder/utils/utils_css\";\nimport { ImageSize } from \"../image/image_size\";\nimport { getHtmlStyle } from \"@html_editor/utils/formatting\";\n\nexport class BackgroundImageOption extends BaseOptionComponent {\n    static template = \"html_builder.BackgroundImageOption\";\n    static dependencies = [\"history\", \"backgroundImageOption\"];\n    static components = { ImageSize };\n    setup() {\n        this.editingElement = this.env.getEditingElement();\n        super.setup();\n        // done here because we have direct access to the editing element\n        // (which we don't have in the normalize of the current plugin)\n        this.toggleBgImageClasses();\n    }\n    toggleBgImageClasses() {\n        this.dependencies.history.ignoreDOMMutations(() => {\n            const backgroundURL = getBgImageURLFromEl(this.editingElement);\n            this.dependencies.backgroundImageOption.setImageBackground(this.editingElement, backgroundURL);\n        });\n    }\n    showMainColorPicker() {\n        const src = new URL(getBgImageURLFromEl(this.editingElement), window.location.origin);\n        return (\n            src.origin === window.location.origin &&\n            (src.pathname.startsWith(\"/html_editor/shape/\") ||\n                src.pathname.startsWith(\"/web_editor/shape/\"))\n        );\n    }\n    getColorPickerColorNames() {\n        const colorNames = [];\n        for (let nbr = 1; nbr <= 5; nbr++) {\n            const colorName = `c${nbr}`;\n            if (getBackgroundImageColor(this.editingElement, colorName)) {\n                colorNames.push(colorName);\n            }\n        }\n        return colorNames;\n    }\n}\n\nexport function getBackgroundImageColor(editingEl, colorName) {\n    const backgroundImageColor = new URL(\n        getBgImageURLFromEl(editingEl),\n        window.location.origin\n    ).searchParams.get(colorName);\n    if (backgroundImageColor) {\n        return normalizeColor(backgroundImageColor, getHtmlStyle(editingEl.ownerDocument));\n    }\n}\n", "import { getValueFromVar } from \"@html_builder/utils/utils\";\nimport { getBgImageURLFromEl, isBackgroundImageAttribute } from \"@html_builder/utils/utils_css\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { removeOnImageChangeAttrs } from \"@html_editor/utils/image_processing\";\nimport { registry } from \"@web/core/registry\";\nimport { convertCSSColorToRgba } from \"@web/core/utils/colors\";\nimport { getBackgroundImageColor } from \"./background_image_option\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { StyleAction } from \"@html_builder/core/core_builder_action_plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\n/**\n * @typedef {((editingElement: HTMLElement) => void)[]} on_bg_image_hide_handlers\n *\n * @typedef {((editingElement: HTMLElement) => HTMLElement)[]} background_filter_target_providers\n * @typedef {((el: HTMLElement) => HTMLElement)[]} get_target_element_providers\n */\n\nexport class BackgroundImageOptionPlugin extends Plugin {\n    static id = \"backgroundImageOption\";\n    static dependencies = [\"builderActions\", \"media\", \"style\"];\n    static shared = [\n        \"changeEditingEl\",\n        \"setImageBackground\",\n        \"loadReplaceBackgroundImage\",\n        \"applyReplaceBackgroundImage\",\n        \"removeBackgroundImage\",\n    ];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_actions: {\n            SelectFilterColorAction,\n            ToggleBgImageAction,\n            RemoveBgImageAction,\n            ReplaceBgImageAction,\n            DynamicColorAction,\n        },\n        content_not_editable_selectors: \".o_we_bg_filter\",\n        system_node_selectors: \".o_we_bg_filter\",\n        get_target_element_providers: withSequence(5, (el) => el),\n    };\n    /**\n     * Transfers the background-image and the dataset information relative to\n     * this image from the old editing element to the new one.\n     * @param {HTMLElement} oldEditingEl - The old editing element.\n     * @param {HTMLElement} newEditingEl - The new editing element.\n     */\n    changeEditingEl(oldEditingEl, newEditingEl) {\n        // When we change the target of this option we need to transfer the\n        // background-image and the dataset information relative to this image\n        // from the old target to the new one.\n        const oldBgURL = getBgImageURLFromEl(oldEditingEl);\n        const isModifiedImage = oldEditingEl.classList.contains(\"o_modified_image_to_save\");\n        const filteredOldDataset = Object.entries(oldEditingEl.dataset).filter(([key]) =>\n            isBackgroundImageAttribute(key)\n        );\n        // Delete the dataset information relative to the background-image of\n        // the old target.\n        for (const [key] of filteredOldDataset) {\n            delete oldEditingEl.dataset[key];\n        }\n        // It is important to delete \".o_modified_image_to_save\" from the old\n        // target as its image source will be deleted.\n        oldEditingEl.classList.remove(\"o_modified_image_to_save\");\n        const filterColorAction = this.dependencies.builderActions.getAction(\"selectFilterColor\");\n        const editingElement = this.getResource(\"get_target_element_providers\")[0](oldEditingEl);\n        const filter = filterColorAction.getValue({ editingElement });\n        this.setImageBackground(oldEditingEl, \"\");\n        if (filter) {\n            filterColorAction.apply({\n                editingElement,\n                value: filter,\n            });\n        }\n        // Apply the changes on the new editing element\n        if (oldBgURL) {\n            this.setImageBackground(newEditingEl, oldBgURL);\n            for (const [key, value] of filteredOldDataset) {\n                newEditingEl.dataset[key] = value;\n            }\n            newEditingEl.classList.toggle(\"o_modified_image_to_save\", isModifiedImage);\n        }\n    }\n    loadReplaceBackgroundImage({ editingElement }) {\n        return new Promise((resolve) => {\n            const onClose = this.dependencies.media.openMediaDialog({\n                onlyImages: true,\n                node: editingElement,\n                save: async (imageEl) => {\n                    resolve(imageEl);\n                },\n            });\n            onClose.then(resolve);\n        });\n    }\n    applyReplaceBackgroundImage({\n        editingElement,\n        loadResult: imageEl,\n        params: { forceClean = false },\n    }) {\n        if (!forceClean && !imageEl) {\n            // Do nothing: no images has been selected on the media dialog\n            return;\n        }\n        const src = imageEl?.src || \"\";\n        this.setImageBackground(editingElement, src);\n        for (const attr of removeOnImageChangeAttrs) {\n            delete editingElement.dataset[attr];\n        }\n        if (imageEl) {\n            if (src.startsWith(\"data:\")) {\n                editingElement.classList.add(\"o_modified_image_to_save\");\n            }\n            Object.assign(editingElement.dataset, imageEl.dataset);\n        }\n    }\n    /**\n     *\n     * @param {HTMLElement} el\n     * @param {String} backgroundURL\n     */\n    setImageBackground(el, backgroundURL) {\n        if (backgroundURL) {\n            el.classList.add(\"oe_img_bg\", \"o_bg_img_center\", \"o_bg_img_origin_border_box\");\n        } else {\n            const editingElement = this.getResource(\"get_target_element_providers\")[0](el);\n            this.dependencies.builderActions\n                .getAction(\"selectFilterColor\")\n                .apply({ editingElement });\n            el.classList.remove(\n                \"oe_img_bg\",\n                \"o_bg_img_center\",\n                \"o_bg_img_origin_border_box\",\n                \"o_modified_image_to_save\"\n            );\n        }\n        // TODO: check this comment\n        // We use selectStyle so that if when a background image is removed the\n        // remaining image matches the o_cc's gradient background, it can be\n        // removed too.\n        this.dependencies.style.setBackgroundImageUrl(el, backgroundURL);\n    }\n    /**\n     * Remove the current background image and notify listeners.\n     *\n     * @param {Object} context\n     * @param {HTMLElement} context.editingElement\n     * @param {Object} [context.params]\n     */\n    removeBackgroundImage({ editingElement, params }) {\n        this.applyReplaceBackgroundImage({\n            editingElement,\n            loadResult: \"\",\n            params: { ...params, forceClean: true },\n        });\n        this.dispatchTo(\"on_bg_image_hide_handlers\", editingElement);\n    }\n}\n\nexport class SelectFilterColorAction extends StyleAction {\n    static id = \"selectFilterColor\";\n    static dependencies = [\"color\", \"backgroundImageOption\", \"builderActions\"];\n    apply({ editingElement, value }) {\n        // Find the filter element.\n        let filterEl = editingElement.querySelector(\":scope > .o_we_bg_filter\");\n\n        // If the filter would be transparent, remove it / don't create it.\n        const rgba = value && convertCSSColorToRgba(value);\n        if (!value || (rgba && rgba.opacity < 0.001)) {\n            if (filterEl) {\n                filterEl.remove();\n            }\n            return;\n        }\n\n        // Create the filter if necessary.\n        if (!filterEl) {\n            filterEl = document.createElement(\"div\");\n            filterEl.classList.add(\"o_we_bg_filter\");\n            let lastBackgroundEl;\n            for (const fn of this.getResource(\"background_filter_target_providers\")) {\n                lastBackgroundEl = fn(editingElement);\n                if (lastBackgroundEl) {\n                    break;\n                }\n            }\n            if (lastBackgroundEl) {\n                lastBackgroundEl.insertAdjacentElement(\"afterend\", filterEl);\n            } else {\n                editingElement.prepend(filterEl);\n            }\n        }\n        this.dependencies.builderActions.getAction(\"styleAction\").apply({\n            editingElement: filterEl,\n            params: {\n                mainParam: \"background-color\",\n            },\n            value: value,\n        });\n    }\n    getValue({ editingElement }) {\n        const filterEl = editingElement.querySelector(\":scope > .o_we_bg_filter\");\n        if (!filterEl) {\n            return \"\";\n        }\n        return super.getValue({\n            editingElement: filterEl,\n            params: {\n                mainParam: \"background-color\",\n            },\n        });\n    }\n}\n\nexport class ToggleBgImageAction extends BuilderAction {\n    static id = \"toggleBgImage\";\n    static dependencies = [\"backgroundImageOption\"];\n    load(context) {\n        return this.dependencies.backgroundImageOption.loadReplaceBackgroundImage(context);\n    }\n    apply(context) {\n        return this.dependencies.backgroundImageOption.applyReplaceBackgroundImage(context);\n    }\n    isApplied({ editingElement }) {\n        return !!getBgImageURLFromEl(editingElement);\n    }\n    clean(context) {\n        this.dependencies.backgroundImageOption.removeBackgroundImage(context);\n    }\n}\n\nexport class RemoveBgImageAction extends BuilderAction {\n    static id = \"removeBgImage\";\n    static dependencies = [\"backgroundImageOption\"];\n    apply(context) {\n        this.dependencies.backgroundImageOption.removeBackgroundImage(context);\n    }\n}\n\nexport class ReplaceBgImageAction extends BuilderAction {\n    static id = \"replaceBgImage\";\n    static dependencies = [\"backgroundImageOption\"];\n    load(context) {\n        return this.dependencies.backgroundImageOption.loadReplaceBackgroundImage(context);\n    }\n    apply(context) {\n        return this.dependencies.backgroundImageOption.applyReplaceBackgroundImage(context);\n    }\n}\nexport class DynamicColorAction extends BuilderAction {\n    static id = \"dynamicColor\";\n    static dependencies = [\"backgroundImageOption\"];\n    getValue({ editingElement, params: { mainParam: colorName } }) {\n        return getBackgroundImageColor(editingElement, colorName);\n    }\n    apply({ editingElement, params: { mainParam: colorName }, value }) {\n        value = getValueFromVar(value);\n        const currentSrc = getBgImageURLFromEl(editingElement);\n        const newURL = new URL(currentSrc, window.location.origin);\n        newURL.searchParams.set(colorName, value);\n        const src = newURL.pathname + newURL.search;\n        this.dependencies.backgroundImageOption.setImageBackground(editingElement, src);\n    }\n}\n\nregistry\n    .category(\"builder-plugins\")\n    .add(BackgroundImageOptionPlugin.id, BackgroundImageOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { BackgroundImageOption } from \"./background_image_option\";\nimport { BackgroundPositionOption } from \"./background_position_option\";\nimport { BackgroundShapeOption } from \"./background_shape_option\";\nimport { useBackgroundOption } from \"./background_hook\";\nimport { ImageFilterOption } from \"@html_builder/plugins/image/image_filter_option\";\nimport { ImageFormatOption } from \"@html_builder/plugins/image/image_format_option\";\n\nexport class BackgroundOption extends BaseOptionComponent {\n    static template = \"html_builder.BackgroundOption\";\n    static components = {\n        BackgroundImageOption,\n        BackgroundPositionOption,\n        BackgroundShapeOption,\n        ImageFilterOption,\n        ImageFormatOption,\n    };\n    static props = {\n        withColors: { type: Boolean },\n        withImages: { type: Boolean },\n        withColorCombinations: { type: Boolean },\n        withShapes: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        withShapes: false,\n    };\n\n    setup() {\n        super.setup();\n        const { showColorFilter } = useBackgroundOption(this.isActiveItem);\n        this.showColorFilter = showColorFilter;\n    }\n    computeMaxDisplayWidth() {\n        return 1920;\n    }\n}\n", "import { applyFunDependOnSelectorAndExclude } from \"@html_builder/plugins/utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\n\n/** @typedef {import(\"plugins\").CSSSelector} CSSSelector */\n/**\n * @typedef {{\n *     selector: CSSSelector;\n *     exclude?: CSSSelector;\n *     applyTo?: CSSSelector;\n * }[]} mark_color_level_selector_params\n */\n\nclass BackgroundOptionPlugin extends Plugin {\n    static id = \"backgroundOption\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        normalize_handlers: this.normalize.bind(this),\n        system_classes: [\"o_colored_level\"],\n    };\n    normalize(root) {\n        const markColorLevelSelectorParams = this.getResource(\"mark_color_level_selector_params\");\n        for (const markColorLevelSelectorParam of markColorLevelSelectorParams) {\n            applyFunDependOnSelectorAndExclude(\n                this.markColorLevel,\n                root,\n                markColorLevelSelectorParam\n            );\n        }\n    }\n    markColorLevel(editingEl) {\n        editingEl.classList.add(\"o_colored_level\");\n    }\n}\nregistry.category(\"builder-plugins\").add(BackgroundOptionPlugin.id, BackgroundOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class BackgroundPositionOption extends BaseOptionComponent {\n    static template = \"html_builder.BackgroundPositionOption\";\n}\n", "import { getBgImageURLFromEl } from \"@html_builder/utils/utils_css\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { BackgroundPositionOverlay } from \"./background_position_overlay\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { loadImage } from \"@html_editor/utils/image_processing\";\n\nconst getBgSizeValue = function ({ editingElement, params: { mainParam: styleName } }) {\n    const backgroundSize = editingElement.style.backgroundSize;\n    const bgWidthAndHeight = backgroundSize.split(/\\s+/g);\n    const value = styleName === \"width\" ? bgWidthAndHeight[0] : bgWidthAndHeight[1] || \"\";\n    return value === \"auto\" ? \"\" : value;\n};\n\nclass BackgroundPositionOptionPlugin extends Plugin {\n    static id = \"backgroundPositionOption\";\n    static dependencies = [\"overlay\", \"overlayButtons\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_actions: {\n            BackgroundTypeAction,\n            SetBackgroundSizeAction,\n            BackgroundPositionOverlayAction,\n        },\n    };\n}\n\nexport class BackgroundTypeAction extends BuilderAction {\n    static id = \"backgroundType\";\n    apply({ editingElement, value }) {\n        editingElement.classList.toggle(\"o_bg_img_opt_repeat\", value === \"repeat-pattern\");\n        editingElement.style.setProperty(\"background-position\", \"\");\n        editingElement.style.setProperty(\n            \"background-size\",\n            value !== \"repeat-pattern\" ? \"\" : \"100px\"\n        );\n    }\n    isApplied({ editingElement, value }) {\n        const hasElRepeatStyle = getComputedStyle(editingElement).backgroundRepeat === \"repeat\";\n        return value === \"repeat-pattern\" ? hasElRepeatStyle : !hasElRepeatStyle;\n    }\n}\n\nexport class SetBackgroundSizeAction extends BuilderAction {\n    static id = \"setBackgroundSize\";\n    getValue(context) {\n        return getBgSizeValue(context);\n    }\n    apply({ editingElement, params: { mainParam: styleName }, value }) {\n        const otherParam = styleName === \"width\" ? \"height\" : \"width\";\n        let otherBgSize = getBgSizeValue({\n            editingElement: editingElement,\n            params: { mainParam: otherParam },\n        });\n        let bgSize;\n        value ||= \"auto\";\n        if (styleName === \"width\") {\n            otherBgSize = otherBgSize === \"\" ? \"\" : ` ${otherBgSize}`;\n            bgSize = `${value}${otherBgSize}`;\n        } else {\n            otherBgSize ||= \"auto\";\n            bgSize = `${otherBgSize} ${value}`;\n        }\n        editingElement.style.setProperty(\"background-size\", bgSize);\n    }\n}\n\nexport class BackgroundPositionOverlayAction extends BuilderAction {\n    static id = \"backgroundPositionOverlay\";\n    static dependencies = [\"overlayButtons\", \"history\"];\n    setup() {\n        this.withLoadingEffect = false;\n    }\n    async load({ editingElement }) {\n        const imgEl = await loadImage(getBgImageURLFromEl(editingElement));\n        return new Promise((resolve) => {\n            // Hide the builder overlay buttons when the user changes\n            // the background position.\n            this.dependencies.overlayButtons.hideOverlayButtonsUi();\n            let appliedBgPosition = \"\";\n            const onRemove = () => {\n                this.dependencies.overlayButtons.showOverlayButtonsUi();\n                resolve(appliedBgPosition);\n            };\n            const removeOverlay = this.services.overlay.add(\n                BackgroundPositionOverlay,\n                {\n                    editingElement: editingElement,\n                    mockEditingElOnImg: imgEl,\n                    applyPosition: (bgPosition) => {\n                        appliedBgPosition = bgPosition;\n                        removeOverlay();\n                    },\n                    discardPosition: () => removeOverlay(),\n                    editable: this.editable,\n                    history: {\n                        makeSavePoint: this.dependencies.history.makeSavePoint,\n                    },\n                },\n                { onRemove }\n            );\n        });\n    }\n    apply({ editingElement, loadResult: bgPosition }) {\n        if (bgPosition) {\n            editingElement.style.backgroundPosition = bgPosition;\n        }\n    }\n}\n\nregistry\n    .category(\"builder-plugins\")\n    .add(BackgroundPositionOptionPlugin.id, BackgroundPositionOptionPlugin);\n", "import { scrollTo } from \"@html_builder/utils/scrolling\";\nimport {\n    Component,\n    onMounted,\n    onWillStart,\n    onWillUnmount,\n    useEffect,\n    useExternalListener,\n    useRef,\n} from \"@odoo/owl\";\n\nexport class BackgroundPositionOverlay extends Component {\n    static template = \"html_builder.BackgroundPositionOverlay\";\n    static props = {\n        editingElement: { validate: (p) => p.nodeType === Node.ELEMENT_NODE },\n        mockEditingElOnImg: { validate: (p) => p.tagName === \"IMG\" },\n        applyPosition: { type: Function },\n        discardPosition: { type: Function },\n        editable: { validate: (p) => p.nodeType === Node.ELEMENT_NODE },\n        history: Object,\n    };\n\n    setup() {\n        this.backgroundOverlayRef = useRef(\"backgroundOverlay\");\n        this.overlayMaskRef = useRef(\"overlayMask\");\n        this.overlayContentRef = useRef(\"overlayContent\");\n        this.bgDraggerRef = useRef(\"bgDragger\");\n\n        this.iframe = this.props.editable.ownerDocument.defaultView.frameElement;\n        this.builderOverlayContainerEl = document.querySelector(\n            \"[data-oe-local-overlay-id='builder-overlay-container']:not(:empty)\"\n        );\n        // If there is a Scroll Effect, a span.s_parallax_bg inside the section\n        // contains the background. Otherwise it's the section itself.\n        // And targetContainerEl should always be the section.\n        this.targetContainerEl = this.props.editingElement.classList.contains(\"s_parallax_bg\")\n            ? this.props.editingElement.parentElement\n            : this.props.editingElement;\n\n        this._dimensionOverlay = this.dimensionOverlay.bind(this);\n\n        // Discard when clicking anywhere on the page\n        const editableDocument = this.props.editable.ownerDocument;\n        useExternalListener(editableDocument, \"pointerdown\", this.discard.bind(this));\n        useExternalListener(document, \"pointerdown\", this.discard.bind(this));\n\n        useExternalListener(window, \"resize\", this._dimensionOverlay);\n        useExternalListener(this.iframe.contentWindow, \"resize\", this._dimensionOverlay);\n        useExternalListener(this.iframe.contentWindow, \"scroll\", this._dimensionOverlay);\n\n        onWillStart(async () => {\n            const position = getComputedStyle(this.props.editingElement)\n                .backgroundPosition.split(\" \")\n                .map((v) => parseInt(v));\n            const delta = this.getBackgroundDelta();\n            // originalPosition kept in % for when movement in one direction\n            // doesn't make sense.\n            this.originalPosition = { left: position[0], top: position[1] };\n            // Convert % values to pixels for current position because\n            // mouse movement is in pixels.\n            this.currentPosition = {\n                left: (position[0] / 100) * delta.x || 0,\n                top: (position[1] / 100) * delta.y || 0,\n            };\n            // Make sure the editing element is visible\n            const rect = this.targetContainerEl.getBoundingClientRect();\n            const isEditingElEntirelyVisible =\n                rect.top >= 0 &&\n                rect.bottom <= this.targetContainerEl.ownerDocument.defaultView.innerHeight;\n            if (!isEditingElEntirelyVisible) {\n                await scrollTo(this.targetContainerEl, { extraOffset: 50 });\n            }\n        });\n\n        onMounted(() => {\n            this.reloadSavePoint = this.props.history.makeSavePoint();\n            this.dimensionOverlay();\n            this.targetContainerEl.classList.add(\"o_we_background_positioning\");\n        });\n\n        useEffect(() => {\n            this.tooltip = window.Tooltip.getOrCreateInstance(this.bgDraggerRef.el, {\n                trigger: \"manual\",\n                container: this.backgroundOverlayRef.el,\n            });\n            this.tooltip.show();\n        });\n\n        onWillUnmount(() => {\n            this.builderOverlayContainerEl.style.clipPath = \"\";\n            this.tooltip.dispose();\n        });\n    }\n\n    apply() {\n        const position = getComputedStyle(this.props.editingElement).backgroundPosition;\n        this.reloadSavePoint();\n        this.props.applyPosition(position);\n    }\n\n    discard() {\n        this.reloadSavePoint();\n        this.props.discardPosition();\n    }\n\n    onWheel(ev) {\n        if (ev.ctrlKey) {\n            return;\n        }\n        this.iframe.contentWindow.scrollBy(ev.deltaX, ev.deltaY);\n    }\n\n    onDragBackgroundStart(ev) {\n        this.backgroundOverlayRef.el.classList.add(\"o_we_grabbing\");\n        const documentEl = window.document;\n        const onDragBackgroundMove = this.onDragBackgroundMove.bind(this);\n        documentEl.addEventListener(\"mousemove\", onDragBackgroundMove);\n        documentEl.addEventListener(\n            \"mouseup\",\n            () => {\n                this.backgroundOverlayRef.el.classList.remove(\"o_we_grabbing\");\n                documentEl.removeEventListener(\"mousemove\", onDragBackgroundMove);\n            },\n            { once: true }\n        );\n    }\n\n    /**\n     * Drags the overlay's background image.\n     *\n     */\n    onDragBackgroundMove(ev) {\n        ev.preventDefault();\n\n        const delta = this.getBackgroundDelta();\n        this.currentPosition.left = clamp(this.currentPosition.left + ev.movementX, [0, delta.x]);\n        this.currentPosition.top = clamp(this.currentPosition.top + ev.movementY, [0, delta.y]);\n\n        const percentPosition = {\n            left: (this.currentPosition.left / delta.x) * 100,\n            top: (this.currentPosition.top / delta.y) * 100,\n        };\n        // In cover mode, one delta will be 0 and dividing by it will yield\n        // Infinity. Defaulting to originalPosition in that case (can't be\n        // dragged).\n        percentPosition.left = isFinite(percentPosition.left)\n            ? percentPosition.left\n            : this.originalPosition.left;\n        percentPosition.top = isFinite(percentPosition.top)\n            ? percentPosition.top\n            : this.originalPosition.top;\n\n        this.props.editingElement.style.backgroundPosition = `${percentPosition.left}% ${percentPosition.top}%`;\n\n        function clamp(val, bounds) {\n            // We sort the bounds because when one dimension of the rendered\n            // background is larger than the container, delta is negative, and\n            // we want to use it as lower bound.\n            bounds = bounds.sort();\n            return Math.max(bounds[0], Math.min(val, bounds[1]));\n        }\n    }\n\n    dimensionOverlay() {\n        const iframeRect = this.iframe.getBoundingClientRect();\n        const targetContainerRect = this.targetContainerEl.getBoundingClientRect();\n        const scale = this.getIframeContainerScale();\n        const scaledRect = new DOMRect(\n            scale * targetContainerRect.x,\n            scale * targetContainerRect.y,\n            scale * targetContainerRect.width,\n            scale * targetContainerRect.height\n        );\n\n        // Make a cut-out in the overlay mask to highlight the editing element.\n        // \"polygon\" is used because \"rect\" would do the inverse.\n        const clipPath = `polygon(\n            evenodd,\n            0 0, 100% 0,100% 100%, 0 100%, 0 0,\n            ${scaledRect.left}px ${scaledRect.top}px,\n            ${scaledRect.right}px ${scaledRect.top}px,\n            ${scaledRect.right}px ${scaledRect.bottom}px,\n            ${scaledRect.left}px ${scaledRect.bottom}px,\n            ${scaledRect.left}px ${scaledRect.top}px)\n        `;\n        this.overlayMaskRef.el.style.clipPath = clipPath;\n        this.builderOverlayContainerEl.style.clipPath = clipPath;\n\n        // The overlay covers the whole iframe excluding the scrollbar.\n        Object.assign(this.backgroundOverlayRef.el.style, {\n            left: `${iframeRect.left}px`,\n            top: `${iframeRect.top}px`,\n            height: `${this.props.editable.ownerDocument.body.clientHeight * scale}px`,\n            width: `${this.props.editable.ownerDocument.body.clientWidth * scale}px`,\n        });\n\n        // The overlay content covers the editing element.\n        Object.assign(this.overlayContentRef.el.style, {\n            left: `${scaledRect.left}px`,\n            top: `${scaledRect.top}px`,\n        });\n        const overlayButtonsEl = this.overlayContentRef.el.querySelector(\".o_we_overlay_buttons\");\n        overlayButtonsEl.style.top = `${Math.max(0, -scaledRect.top)}px`;\n        this.bgDraggerRef.el.style.setProperty(\"width\", `${scaledRect.width}px`, \"important\");\n        this.bgDraggerRef.el.style.setProperty(\"height\", `${scaledRect.height}px`, \"important\");\n\n        // Refresh tooltip position after overlay reposition\n        if (this.tooltip) {\n            this.tooltip.update();\n        }\n    }\n\n    /**\n     * Gets the scale factor of the iframe's parent container.\n     * Useful when the user zooms in/out in the browser, as it affects the\n     * dimensions of the iframe.\n     * @returns {number} The scale factor (1 if no transform is applied)\n     */\n    getIframeContainerScale() {\n        const matrix = getComputedStyle(this.iframe.parentElement).transform;\n        if (matrix === \"none\") {\n            return 1;\n        }\n        const values = matrix\n            .match(/matrix\\(([^)]+)\\)/)[1]\n            .split(\",\")\n            .map(parseFloat);\n        return values[0];\n    }\n\n    /**\n     * Returns the difference between the editing element's size and the\n     * background's rendered size. Background position values in % are a\n     * percentage of this.\n     *\n     */\n    getBackgroundDelta() {\n        const naturalWidth = this.props.mockEditingElOnImg.naturalWidth;\n        const naturalHeight = this.props.mockEditingElOnImg.naturalHeight;\n        const editingElStyle = getComputedStyle(this.props.editingElement);\n        // If background-attachment: fixed, the background is sized relative to\n        // the page viewport.\n        const bgRect =\n            editingElStyle.backgroundAttachment === \"fixed\"\n                ? this.iframe.getBoundingClientRect()\n                : this.props.editingElement.getBoundingClientRect();\n\n        if (editingElStyle.backgroundSize === \"cover\") {\n            const renderRatio = Math.max(\n                bgRect.width / naturalWidth,\n                bgRect.height / naturalHeight\n            );\n\n            return {\n                x: bgRect.width - Math.round(renderRatio * naturalWidth),\n                y: bgRect.height - Math.round(renderRatio * naturalHeight),\n            };\n        }\n\n        let [width, height] = editingElStyle.backgroundSize.split(\" \");\n        if (width === \"auto\" && (height === \"auto\" || !height)) {\n            return {\n                x: bgRect.width - naturalWidth,\n                y: bgRect.height - naturalHeight,\n            };\n        }\n        // At least one of width or height is not auto, so we can use it to\n        // calculate the other if it's not set.\n        [width, height] = [parseInt(width), parseInt(height)];\n        return {\n            x: bgRect.width - (width || (height * naturalWidth) / naturalHeight),\n            y: bgRect.height - (height || (width * naturalHeight) / naturalWidth),\n        };\n    }\n}\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { toRatio } from \"@html_builder/utils/utils\";\nimport { getBgImageURLFromEl } from \"@html_builder/utils/utils_css\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class BackgroundShapeOption extends BaseOptionComponent {\n    static template = \"html_builder.BackgroundShapeOption\";\n    static dependencies = [\"backgroundShapeOption\"];\n    setup() {\n        super.setup();\n        this.backgroundShapePlugin = this.dependencies.backgroundShapeOption;\n        this.toRatio = toRatio;\n        this.state = useDomState((editingElement) => {\n            const shapeData = this.backgroundShapePlugin.getShapeData(editingElement);\n            const shapeInfo = this.backgroundShapePlugin.getBackgroundShapes()[shapeData.shape];\n            return {\n                shapeName: shapeInfo?.selectLabel || _t(\"None\"),\n                isAnimated: shapeInfo?.animated,\n            };\n        });\n    }\n    showBackgroundShapes() {\n        this.backgroundShapePlugin.showBackgroundShapes(this.env.getEditingElements());\n    }\n    getDefaultColorNames() {\n        const editingEl = this.env.getEditingElement();\n        return Object.keys(getDefaultColors(editingEl));\n    }\n}\n\n/**\n * Returns the default colors for the currently selected shape.\n *\n * @param {HTMLElement} editingElement the element on which to read the\n * shape data.\n */\nexport function getDefaultColors(editingElement) {\n    const shapeContainerEl = editingElement.querySelector(\":scope > .o_we_shape\");\n    if (!shapeContainerEl) {\n        return {};\n    }\n    const shapeContainerClonedEl = shapeContainerEl.cloneNode(true);\n    shapeContainerClonedEl.classList.add(\"d-none\");\n    // Needs to be in document for bg-image class to take effect\n    editingElement.ownerDocument.body.appendChild(shapeContainerClonedEl);\n    shapeContainerClonedEl.style.setProperty(\"background-image\", \"\");\n    const shapeSrc = shapeContainerClonedEl && getBgImageURLFromEl(shapeContainerClonedEl);\n    shapeContainerClonedEl.remove();\n    if (!shapeSrc) {\n        return {};\n    }\n    const url = new URL(shapeSrc, window.location.origin);\n    return Object.fromEntries(url.searchParams.entries());\n}\n", "import { getValueFromVar } from \"@html_builder/utils/utils\";\nimport { normalizeColor } from \"@html_builder/utils/utils_css\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { backgroundShapesDefinition } from \"./background_shapes_definition\";\nimport { ShapeSelector } from \"@html_builder/plugins/shape/shape_selector\";\nimport { getDefaultColors } from \"./background_shape_option\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { getBgImageURLFromURL } from \"@html_editor/utils/image\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { getHtmlStyle } from \"@html_editor/utils/formatting\";\n\n/**\n * @typedef {((editingElement: HTMLElement) => HTMLElement)[]} background_shape_target_providers\n */\n\nexport class BackgroundShapeOptionPlugin extends Plugin {\n    static id = \"backgroundShapeOption\";\n    static dependencies = [\"customizeTab\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_actions: {\n            SetBackgroundShapeAction,\n            ToggleBgShapeAction,\n            ShowOnMobileAction,\n            FlipShapeAction,\n            SetBgAnimationSpeedAction,\n            BackgroundShapeColorAction,\n        },\n        background_shape_target_providers: withSequence(5, (editingElement) =>\n            editingElement.querySelector(\":scope > .o_we_bg_filter\")\n        ),\n        content_not_editable_selectors: \".o_we_shape\",\n        system_node_selectors: \".o_we_shape\",\n    };\n    static shared = [\n        \"getShapeStyleUrl\",\n        \"getShapeData\",\n        \"showBackgroundShapes\",\n        \"getBackgroundShapes\",\n        \"getImplicitColors\",\n        \"applyShape\",\n        \"createShapeContainer\",\n    ];\n    setup() {\n        // TODO: update shapeBackgroundImagePerClass if a stylesheet value\n        // changes.\n        this.shapeBackgroundImagePerClass = {};\n        for (const styleSheet of this.document.styleSheets) {\n            if (styleSheet.href && new URL(styleSheet.href).host !== location.host) {\n                // In some browsers, if a stylesheet is loaded from a different\n                // domain accessing cssRules results in a SecurityError.\n                continue;\n            }\n            for (const rule of [...styleSheet.cssRules]) {\n                if (rule.selectorText && rule.selectorText.startsWith(\".o_we_shape.\")) {\n                    this.shapeBackgroundImagePerClass[rule.selectorText] =\n                        rule.style.backgroundImage;\n                }\n            }\n        }\n        // Flip classes should no longer be used but are still present in some\n        // theme snippets.\n        const flipEls = [...this.editable.querySelectorAll(\".o_we_flip_x, .o_we_flip_y\")];\n        for (const flipEl of flipEls) {\n            this.applyShape(flipEl, () => ({ flip: this.getShapeData(flipEl).flip }));\n        }\n    }\n    /**\n     * Handles everything related to saving state before preview and restoring\n     * it after a preview or locking in the changes when not in preview.\n     *\n     * @param {HTMLElement} editingElement - The element being edited\n     * @param {Function} computeShapeData - Function to compute the new shape\n     * data.\n     */\n    applyShape(editingElement, computeShapeData) {\n        const newShapeData = computeShapeData();\n        const changedShape = !!newShapeData.shape;\n        this.markShape(editingElement, newShapeData);\n\n        // Updates/removes the shape container as needed and gives it the\n        // correct background shape\n        const json = editingElement.dataset.oeShapeData;\n        const {\n            shape,\n            colors,\n            flip = [],\n            animated = \"false\",\n            showOnMobile,\n            shapeAnimationSpeed,\n        } = json ? JSON.parse(json) : {};\n\n        let shapeContainerEl = editingElement.querySelector(\":scope > .o_we_shape\");\n\n        if (!shape) {\n            return this.insertShapeContainer(editingElement, null);\n        }\n\n        if (changedShape) {\n            // Reset shape container when shape changes (e.g., for transparent\n            // color)\n            shapeContainerEl = this.createShapeContainer(editingElement, shape);\n        }\n\n        // Remove old flip classes (flipping is now done via SVG)\n        shapeContainerEl.classList.remove(\"o_we_flip_x\", \"o_we_flip_y\");\n\n        shapeContainerEl.classList.toggle(\"o_we_animated\", animated === \"true\");\n\n        const shouldCustomize =\n            Boolean(colors) || flip.length > 0 || parseFloat(shapeAnimationSpeed) !== 0;\n\n        if (shouldCustomize) {\n            // Apply custom image, flip, speed\n            shapeContainerEl.style.setProperty(\n                \"background-image\",\n                `url(\"${this.getShapeSrc(editingElement)}\")`\n            );\n            shapeContainerEl.style.backgroundPosition = \"\";\n\n            if (flip.length) {\n                let [xPos, yPos] = getComputedStyle(shapeContainerEl)\n                    .backgroundPosition.split(\" \")\n                    .map(parseFloat);\n\n                xPos = flip.includes(\"x\") ? -xPos + 100 : xPos;\n                yPos = flip.includes(\"y\") ? -yPos + 100 : yPos;\n\n                shapeContainerEl.style.backgroundPosition = `${xPos}% ${yPos}%`;\n            }\n        } else {\n            // Let CSS class define the shape\n            shapeContainerEl.style.setProperty(\"background-image\", \"\");\n            shapeContainerEl.style.setProperty(\"background-position\", \"\");\n        }\n\n        shapeContainerEl.classList.toggle(\"o_shape_show_mobile\", Boolean(showOnMobile));\n    }\n\n    /**\n     * Creates and inserts a container for the shape with the right classes.\n     *\n     * @param {HTMLElement} editingElement - The element to which the shape is attached.\n     * @param {string} shape - The shape name used to generate a class.\n     * @returns {HTMLElement} The created shape container element.\n     */\n    createShapeContainer(editingElement, shape) {\n        const shapeContainer = this.insertShapeContainer(\n            editingElement,\n            document.createElement(\"div\")\n        );\n        editingElement.style.setProperty(\"position\", \"relative\");\n        shapeContainer.className = `o_we_shape o_${shape.replace(/\\//g, \"_\")}`;\n        return shapeContainer;\n    }\n    /**\n     * Returns the implicit colors for the currently selected shape.\n     *\n     * The implicit colors are use upon shape selection. They are computed as:\n     * - the default colors\n     * - patched with each set of colors of previous siblings shape\n     * - patched with the colors of the previously selected shape\n     * - filtered to only keep the colors involved in the current shape\n     *\n     * @param {HTMLElement} editingElement\n     * @param {String} shapeName identifier of the selected shape.\n     * @param {Object} previousColors colors of the shape before its\n     * replacement.\n     */\n    getImplicitColors(editingElement, shapeName, previousColors = {}) {\n        const selectedBackgroundUrl = this.getShapeStyleUrl(shapeName);\n        const defaultColors = this.getShapeDefaultColors(selectedBackgroundUrl);\n        let colors = previousColors;\n        let sibling = editingElement.previousElementSibling;\n        while (sibling) {\n            colors = Object.assign(this.getShapeData(sibling).colors || {}, colors);\n            sibling = sibling.previousElementSibling;\n        }\n        const defaultKeys = Object.keys(defaultColors);\n        colors = Object.assign(defaultColors, colors);\n        return pick(colors, ...defaultKeys);\n    }\n    /**\n     * Returns the default colors for the a shape in the selector.\n     *\n     * @param {String} selectedBackgroundUrl\n     */\n    getShapeDefaultColors(selectedBackgroundUrl) {\n        const shapeSrc = selectedBackgroundUrl && getBgImageURLFromURL(selectedBackgroundUrl);\n        const url = new URL(shapeSrc, window.location.origin);\n        return Object.fromEntries(url.searchParams.entries());\n    }\n    /**\n     * Retrieves current shape data from the target's dataset.\n     *\n     * @param {HTMLElement} editingElement the target on which to read the shape\n     * data.\n     */\n    getShapeData(editingElement) {\n        const defaultData = {\n            shape: \"\",\n            colors: getDefaultColors(editingElement),\n            flip: [],\n            showOnMobile: false,\n            shapeAnimationSpeed: \"0\",\n        };\n        const json = editingElement.dataset.oeShapeData;\n        if (json) {\n            Object.assign(defaultData, JSON.parse(json.replace(/'/g, '\"')));\n            // Compatibility with old shapes.\n            defaultData.shape = defaultData.shape.replace(\"web_editor\", \"html_builder\");\n        }\n        return defaultData;\n    }\n    /**\n     * Returns the src of the shape corresponding to the current parameters.\n     *\n     * @param {HTMLElement} editingElement\n     */\n    getShapeSrc(editingElement) {\n        const { shape, colors, flip, shapeAnimationSpeed } = this.getShapeData(editingElement);\n        if (!shape) {\n            return \"\";\n        }\n        const searchParams = Object.entries(colors).map(([colorName, colorValue]) => {\n            const encodedCol = encodeURIComponent(colorValue);\n            return `${colorName}=${encodedCol}`;\n        });\n        if (flip.length) {\n            searchParams.push(`flip=${encodeURIComponent(flip.sort().join(\"\"))}`);\n        }\n        if (Number(shapeAnimationSpeed)) {\n            searchParams.push(`shapeAnimationSpeed=${encodeURIComponent(shapeAnimationSpeed)}`);\n        }\n        return `/html_editor/shape/${encodeURIComponent(shape)}.svg?${searchParams.join(\"&\")}`;\n    }\n    /**\n     *\n     * @param {String} shapeId\n     */\n    getShapeStyleUrl(shapeId) {\n        const shapeClassName = `o_${shapeId.replace(/\\//g, \"_\")}`;\n        // Match current palette\n        return this.shapeBackgroundImagePerClass[`.o_we_shape.${shapeClassName}`];\n    }\n    /**\n     * Inserts or removes the given container at the right position in the\n     * document.\n     *\n     * @param {HTMLElement} editingElement\n     * @param {HTMLElement} newContainer container to insert, null to remove\n     */\n    insertShapeContainer(editingElement, newContainer) {\n        const shapeContainerEl = editingElement.querySelector(\":scope > .o_we_shape\");\n        if (shapeContainerEl) {\n            this.removeShapeEl(shapeContainerEl);\n        }\n        if (newContainer) {\n            let preShapeLayerEl;\n            for (const fn of this.getResource(\"background_shape_target_providers\")) {\n                preShapeLayerEl = fn(editingElement);\n                if (preShapeLayerEl) {\n                    break;\n                }\n            }\n            if (preShapeLayerEl) {\n                preShapeLayerEl.insertAdjacentElement(\"afterend\", newContainer);\n            } else {\n                editingElement.prepend(newContainer);\n            }\n        }\n        return newContainer;\n    }\n    /**\n     * Overwrites shape properties with the specified data.\n     *\n     * @param {HTMLElement} editingElement\n     * @param {Object} newData an object with the new data\n     */\n    markShape(editingElement, newData) {\n        const defaultColors = getDefaultColors(editingElement);\n        const shapeData = Object.assign(this.getShapeData(editingElement), newData);\n        const areColorsDefault = Object.entries(shapeData.colors).every(\n            ([colorName, colorValue]) =>\n                defaultColors[colorName] &&\n                colorValue.toLowerCase() === defaultColors[colorName].toLowerCase()\n        );\n        if (areColorsDefault) {\n            delete shapeData.colors;\n        }\n        if (!shapeData.shape) {\n            delete editingElement.dataset.oeShapeData;\n        } else {\n            editingElement.dataset.oeShapeData = JSON.stringify(shapeData);\n        }\n    }\n    /**\n     *\n     * @param {HTMLElement} shapeEl\n     */\n    removeShapeEl(shapeEl) {\n        shapeEl.remove();\n    }\n    showBackgroundShapes(editingElements) {\n        this.dependencies.customizeTab.openCustomizeComponent(ShapeSelector, editingElements, {\n            shapeActionId: \"setBackgroundShape\",\n            buttonWrapperClassName: \"o-hb-bg-shape-btn\",\n            selectorTitle: _t(\"Background Shapes\"),\n            shapeGroups: this.getBackgroundShapeGroups(),\n            imgThroughDiv: true,\n            getShapeUrl: this.getShapeStyleUrl.bind(this),\n        });\n    }\n    getBackgroundShapeGroups() {\n        return backgroundShapesDefinition;\n    }\n    getBackgroundShapes() {\n        if (!this.backgroundShapesById) {\n            const entries = Object.values(this.getBackgroundShapeGroups())\n                .map((x) =>\n                    Object.values(x.subgroups)\n                        .map((x) => Object.entries(x.shapes))\n                        .flat()\n                )\n                .flat();\n            this.backgroundShapesById = Object.fromEntries(entries);\n        }\n        return this.backgroundShapesById;\n    }\n}\n\nclass BaseAnimationAction extends BuilderAction {\n    static id = \"baseAnimation\";\n    static dependencies = [\"backgroundShapeOption\"];\n    setup() {\n        this.applyShape = this.dependencies.backgroundShapeOption.applyShape;\n        this.getShapeData = this.dependencies.backgroundShapeOption.getShapeData;\n        this.getImplicitColors = this.dependencies.backgroundShapeOption.getImplicitColors;\n        this.getBackgroundShapes = this.dependencies.backgroundShapeOption.getBackgroundShapes;\n        this.createShapeContainer = this.dependencies.backgroundShapeOption.createShapeContainer;\n        this.showBackgroundShapes = this.dependencies.backgroundShapeOption.showBackgroundShapes;\n    }\n}\nclass SetBackgroundShapeAction extends BaseAnimationAction {\n    static id = \"setBackgroundShape\";\n    apply({ editingElement, params, value }) {\n        params = params || {};\n        const shapeData = this.getShapeData(editingElement);\n        const applyShapeParams = {\n            shape: value,\n            colors: this.getImplicitColors(editingElement, value, shapeData.colors),\n            flip: [],\n            animated: params.animated,\n            shapeAnimationSpeed: shapeData.shapeAnimationSpeed,\n        };\n        this.applyShape(editingElement, () => applyShapeParams);\n    }\n    isApplied({ editingElement, value }) {\n        const currentShapeApplied = this.getShapeData(editingElement).shape;\n        return currentShapeApplied === value;\n    }\n}\nclass ToggleBgShapeAction extends BaseAnimationAction {\n    static id = \"toggleBgShape\";\n    apply({ editingElement }) {\n        const previousSibling = editingElement.previousElementSibling;\n        let shapeToSelect;\n        const allPossiblesShapesUrl = Object.keys(this.getBackgroundShapes());\n        if (previousSibling) {\n            const previousShape = this.getShapeData(previousSibling).shape;\n            shapeToSelect = allPossiblesShapesUrl.find(\n                (shape, i) => allPossiblesShapesUrl[i - 1] === previousShape\n            );\n        }\n        // If there is no previous sibling, if the previous sibling\n        // had the last shape selected or if the previous shape\n        // could not be found in the possible shapes, default to the\n        // first shape.\n        if (!shapeToSelect) {\n            shapeToSelect = allPossiblesShapesUrl[0];\n        }\n        // Only show on mobile by default if toggled from mobile\n        // view.\n        const showOnMobile = this.config.isMobileView(editingElement);\n        this.createShapeContainer(editingElement, shapeToSelect);\n        const applyShapeParams = {\n            shape: shapeToSelect,\n            colors: this.getImplicitColors(editingElement, shapeToSelect),\n            showOnMobile,\n        };\n        this.applyShape(editingElement, () => applyShapeParams);\n        this.showBackgroundShapes([editingElement]);\n    }\n    clean({ editingElement }) {\n        this.applyShape(editingElement, () => ({ shape: \"\" }));\n    }\n    isApplied({ editingElement }) {\n        return !!this.getShapeData(editingElement).shape;\n    }\n}\nclass ShowOnMobileAction extends BaseAnimationAction {\n    static id = \"showOnMobile\";\n    apply({ editingElement }) {\n        this.applyShape(editingElement, () => ({\n            showOnMobile: false,\n        }));\n    }\n    clean({ editingElement }) {\n        this.applyShape(editingElement, () => ({\n            showOnMobile: true,\n        }));\n    }\n    isApplied({ editingElement }) {\n        return !this.getShapeData(editingElement).showOnMobile;\n    }\n}\nclass FlipShapeAction extends BaseAnimationAction {\n    static id = \"flipShape\";\n    apply({ editingElement, params: { mainParam: axis } }) {\n        this.applyShape(editingElement, () => {\n            const flip = new Set(this.getShapeData(editingElement).flip);\n            flip.add(axis);\n            return { flip: [...flip] };\n        });\n    }\n    clean({ editingElement, params: { mainParam: axis } }) {\n        this.applyShape(editingElement, () => {\n            const flip = new Set(this.getShapeData(editingElement).flip);\n            flip.delete(axis);\n            return { flip: [...flip] };\n        });\n    }\n    isApplied({ editingElement, params: { mainParam: axis } }) {\n        // Compat: flip classes are no longer used but may be\n        // present in client db.\n        const selector = `.o_we_flip_${axis}`;\n        const hasFlipClass = !!editingElement.querySelector(`:scope > .o_we_shape${selector}`);\n        return hasFlipClass || this.getShapeData(editingElement).flip.includes(axis);\n    }\n}\nclass SetBgAnimationSpeedAction extends BaseAnimationAction {\n    static id = \"setBgAnimationSpeed\";\n    apply({ editingElement, value }) {\n        this.applyShape(editingElement, () => ({\n            shapeAnimationSpeed: value,\n        }));\n    }\n    getValue({ editingElement }) {\n        return this.getShapeData(editingElement).shapeAnimationSpeed;\n    }\n}\nclass BackgroundShapeColorAction extends BaseAnimationAction {\n    static id = \"backgroundShapeColor\";\n    getValue({ editingElement, params: { mainParam: colorName } }) {\n        // TODO check if it works when the colorpicker is\n        // implemented.\n        const { shape, colors: customColors } = this.getShapeData(editingElement);\n        const colors = Object.assign(getDefaultColors(editingElement), customColors);\n        const color = shape && colors[colorName];\n        return (color && normalizeColor(color, getHtmlStyle(this.document))) || \"\";\n    }\n    apply({ editingElement, params: { mainParam: colorName }, value }) {\n        this.applyShape(editingElement, () => {\n            value = getValueFromVar(value);\n            const { colors: previousColors } = this.getShapeData(editingElement);\n            const newColor = value || getDefaultColors(editingElement)[colorName];\n            const newColors = Object.assign(previousColors, { [colorName]: newColor });\n            return { colors: newColors };\n        });\n    }\n}\n\nregistry\n    .category(\"builder-plugins\")\n    .add(BackgroundShapeOptionPlugin.id, BackgroundShapeOptionPlugin);\n", "import { _t } from \"@web/core/l10n/translation\";\n\nexport const backgroundShapesDefinition = {\n    basic: {\n        label: _t(\"Basic\"),\n        subgroups: {\n            connections: {\n                label: _t(\"Connections\"),\n                shapes: {\n                    \"html_builder/Connections/01\": { selectLabel: _t(\"Connections 01\") },\n                    \"html_builder/Connections/02\": { selectLabel: _t(\"Connections 02\") },\n                    \"html_builder/Connections/03\": { selectLabel: _t(\"Connections 03\") },\n                    \"html_builder/Connections/04\": { selectLabel: _t(\"Connections 04\") },\n                    \"html_builder/Connections/05\": { selectLabel: _t(\"Connections 05\") },\n                    \"html_builder/Connections/06\": { selectLabel: _t(\"Connections 06\") },\n                    \"html_builder/Connections/07\": { selectLabel: _t(\"Connections 07\") },\n                    \"html_builder/Connections/08\": { selectLabel: _t(\"Connections 08\") },\n                    \"html_builder/Connections/09\": { selectLabel: _t(\"Connections 09\") },\n                    \"html_builder/Connections/10\": { selectLabel: _t(\"Connections 10\") },\n                    \"html_builder/Connections/11\": { selectLabel: _t(\"Connections 11\") },\n                    \"html_builder/Connections/12\": { selectLabel: _t(\"Connections 12\") },\n                    \"html_builder/Connections/13\": { selectLabel: _t(\"Connections 13\") },\n                    \"html_builder/Connections/14\": { selectLabel: _t(\"Connections 14\") },\n                    \"html_builder/Connections/15\": { selectLabel: _t(\"Connections 15\") },\n                    \"html_builder/Connections/16\": { selectLabel: _t(\"Connections 16\") },\n                    \"html_builder/Connections/17\": { selectLabel: _t(\"Connections 17\") },\n                    \"html_builder/Connections/18\": { selectLabel: _t(\"Connections 18\") },\n                    \"html_builder/Connections/19\": { selectLabel: _t(\"Connections 19\") },\n                    \"html_builder/Connections/20\": { selectLabel: _t(\"Connections 20\") },\n                },\n            },\n            containers: {\n                label: _t(\"Containers\"),\n                shapes: {\n                    \"html_builder/Containers/01\": { selectLabel: _t(\"Container 01\") },\n                    \"html_builder/Containers/02\": { selectLabel: _t(\"Container 02\") },\n                    \"html_builder/Containers/03\": { selectLabel: _t(\"Container 03\") },\n                    \"html_builder/Containers/04\": { selectLabel: _t(\"Container 04\") },\n                    \"html_builder/Containers/05\": {\n                        selectLabel: _t(\"Container 05\"),\n                        animated: true,\n                    },\n                    \"html_builder/Containers/06\": {\n                        selectLabel: _t(\"Container 06\"),\n                        animated: true,\n                    },\n                },\n            },\n            bold: {\n                label: _t(\"Bold\"),\n                shapes: {\n                    \"html_builder/Bold/16\": { selectLabel: _t(\"Bold 01\") },\n                    \"html_builder/Bold/21\": { selectLabel: _t(\"Bold 02\") },\n                    \"html_builder/Bold/17\": { selectLabel: _t(\"Bold 03\") },\n                    \"html_builder/Bold/22\": { selectLabel: _t(\"Bold 04\") },\n                    \"html_builder/Bold/13\": { selectLabel: _t(\"Bold 05\") },\n                    \"html_builder/Bold/14\": { selectLabel: _t(\"Bold 06\") },\n                    \"html_builder/Bold/15\": { selectLabel: _t(\"Bold 07\") },\n                    \"html_builder/Bold/01_001\": { selectLabel: _t(\"Bold 08\") },\n                    \"html_builder/Bold/18\": { selectLabel: _t(\"Bold 09\") },\n                    \"html_builder/Bold/19\": { selectLabel: _t(\"Bold 10\") },\n                    \"html_builder/Bold/23\": { selectLabel: _t(\"Bold 11\") },\n                    \"html_builder/Bold/20\": { selectLabel: _t(\"Bold 12\") },\n                },\n            },\n            angular: {\n                label: _t(\"Angular\"),\n                shapes: {\n                    \"html_builder/Angular/01\": { selectLabel: _t(\"Angular 01\") },\n                    \"html_builder/Angular/02\": { selectLabel: _t(\"Angular 02\") },\n                    \"html_builder/Angular/03\": { selectLabel: _t(\"Angular 03\") },\n                    \"html_builder/Angular/04\": { selectLabel: _t(\"Angular 04\") },\n                    \"html_builder/Angular/05\": { selectLabel: _t(\"Angular 05\") },\n                    \"html_builder/Angular/06\": { selectLabel: _t(\"Angular 06\") },\n                    \"html_builder/Angular/07\": { selectLabel: _t(\"Angular 07\") },\n                    \"html_builder/Angular/08\": { selectLabel: _t(\"Angular 08\") },\n                    \"html_builder/Angular/09\": { selectLabel: _t(\"Angular 09\") },\n                    \"html_builder/Floats/07\": { selectLabel: _t(\"Angular 10\"), animated: true },\n                },\n            },\n            blobs: {\n                label: _t(\"Blobs\"),\n                shapes: {\n                    \"html_builder/Blobs/02\": { selectLabel: _t(\"Blob 01\") },\n                    \"html_builder/Blobs/05_001\": { selectLabel: _t(\"Blob 02\") },\n                    \"html_builder/Blobs/03\": { selectLabel: _t(\"Blob 03\") },\n                    \"html_builder/Blobs/06_001\": { selectLabel: _t(\"Blob 04\") },\n                    \"html_builder/Blobs/14\": { selectLabel: _t(\"Blob 05\") },\n                    \"html_builder/Blobs/17\": { selectLabel: _t(\"Blob 06\") },\n                    \"html_builder/Blobs/15\": { selectLabel: _t(\"Blob 07\") },\n                    \"html_builder/Blobs/18\": { selectLabel: _t(\"Blob 08\") },\n                    \"html_builder/Blobs/01_001\": { selectLabel: _t(\"Blob 09\"), animated: true },\n                    \"html_builder/Blobs/16\": { selectLabel: _t(\"Blob 10\") },\n                    \"html_builder/Blobs/04_001\": { selectLabel: _t(\"Blob 11\") },\n                    \"html_builder/Blobs/10_002\": { selectLabel: _t(\"Blob 12\") },\n                    \"html_builder/Blobs/13\": { selectLabel: _t(\"Blob 13\") },\n                    \"html_builder/Floats/03\": { selectLabel: _t(\"Blob 14\"), animated: true },\n                    \"html_builder/Floats/04\": { selectLabel: _t(\"Blob 15\"), animated: true },\n                    \"html_builder/Floats/06\": { selectLabel: _t(\"Blob 16\"), animated: true },\n                },\n            },\n        },\n    },\n    linear: {\n        label: _t(\"Linear\"),\n        subgroups: {\n            airy: {\n                label: _t(\"Airy\"),\n                shapes: {\n                    \"html_builder/Airy/01_001\": { selectLabel: _t(\"Airy 01\") },\n                    \"html_builder/Airy/06_001\": { selectLabel: _t(\"Airy 02\") },\n                    \"html_builder/Airy/02_001\": { selectLabel: _t(\"Airy 03\") },\n                    \"html_builder/Airy/07_001\": { selectLabel: _t(\"Airy 04\") },\n                    \"html_builder/Airy/08_001\": { selectLabel: _t(\"Airy 05\") },\n                    \"html_builder/Airy/10_001\": { selectLabel: _t(\"Airy 06\") },\n                    \"html_builder/Airy/09_001\": { selectLabel: _t(\"Airy 07\") },\n                    \"html_builder/Airy/11_001\": { selectLabel: _t(\"Airy 08\") },\n                    \"html_builder/Airy/16\": { selectLabel: _t(\"Airy 09\") },\n                    \"html_builder/Airy/17\": { selectLabel: _t(\"Airy 10\") },\n                    \"html_builder/Airy/12_002\": { selectLabel: _t(\"Airy 11\"), animated: true },\n                    \"html_builder/Airy/13_002\": { selectLabel: _t(\"Airy 12\"), animated: true },\n                    \"html_builder/Airy/14_001\": { selectLabel: _t(\"Airy 13\") },\n                    \"html_builder/Airy/15\": { selectLabel: _t(\"Airy 14\"), animated: true },\n                },\n            },\n            grids: {\n                label: _t(\"Grids\"),\n                shapes: {\n                    \"html_builder/Grids/01\": { selectLabel: _t(\"Grid 01\") },\n                    \"html_builder/Grids/02\": { selectLabel: _t(\"Grid 02\") },\n                    \"html_builder/Grids/03\": { selectLabel: _t(\"Grid 03\") },\n                    \"html_builder/Grids/04\": { selectLabel: _t(\"Grid 04\") },\n                    \"html_builder/Grids/05\": { selectLabel: _t(\"Grid 05\") },\n                    \"html_builder/Grids/06\": { selectLabel: _t(\"Grid 06\") },\n                    \"html_builder/Grids/07\": { selectLabel: _t(\"Grid 07\") },\n                    \"html_builder/Grids/08\": { selectLabel: _t(\"Grid 08\") },\n                },\n            },\n        },\n    },\n    creative: {\n        label: _t(\"Creative\"),\n        subgroups: {\n            patterns: {\n                label: _t(\"Patterns\"),\n                shapes: {\n                    \"html_builder/Patterns/01\": { selectLabel: _t(\"Pattern 01\") },\n                    \"html_builder/Patterns/02\": { selectLabel: _t(\"Pattern 02\") },\n                    \"html_builder/Patterns/03\": { selectLabel: _t(\"Pattern 03\") },\n                    \"html_builder/Patterns/04\": { selectLabel: _t(\"Pattern 04\") },\n                    \"html_builder/Patterns/05\": { selectLabel: _t(\"Pattern 05\") },\n                    \"html_builder/Floats/12\": { selectLabel: _t(\"Pattern 06\"), animated: true },\n                },\n            },\n            blurry: {\n                label: _t(\"Blurry\"),\n                shapes: {\n                    \"html_builder/Blurry/01\": { selectLabel: _t(\"Blurry 01\") },\n                    \"html_builder/Blurry/02\": { selectLabel: _t(\"Blurry 02\") },\n                    \"html_builder/Blurry/03\": { selectLabel: _t(\"Blurry 03\") },\n                    \"html_builder/Blurry/04\": { selectLabel: _t(\"Blurry 04\") },\n                    \"html_builder/Blurry/05\": { selectLabel: _t(\"Blurry 05\") },\n                    \"html_builder/Blurry/06\": { selectLabel: _t(\"Blurry 06\") },\n                },\n            },\n\n            wavy: {\n                label: _t(\"Wavy\"),\n                shapes: {\n                    \"html_builder/Wavy/03\": { selectLabel: _t(\"Wavy 01\") },\n                    \"html_builder/Wavy/10\": { selectLabel: _t(\"Wavy 02\") },\n                    \"html_builder/Wavy/24\": { selectLabel: _t(\"Wavy 03\"), animated: true },\n                    \"html_builder/Wavy/26\": { selectLabel: _t(\"Wavy 04\"), animated: true },\n                    \"html_builder/Wavy/27\": { selectLabel: _t(\"Wavy 05\"), animated: true },\n                    \"html_builder/Wavy/04\": { selectLabel: _t(\"Wavy 06\") },\n                    \"html_builder/Wavy/11_001\": { selectLabel: _t(\"Wavy 07\") },\n                    \"html_builder/Wavy/18\": { selectLabel: _t(\"Wavy 08\") },\n                    \"html_builder/Wavy/08_001\": { selectLabel: _t(\"Wavy 09\") },\n                    \"html_builder/Wavy/09_001\": { selectLabel: _t(\"Wavy 10\") },\n                    \"html_builder/Wavy/22_001\": { selectLabel: _t(\"Wavy 11\") },\n                    \"html_builder/Wavy/29\": { selectLabel: _t(\"Wavy 12\") },\n                    \"html_builder/Wavy/30\": { selectLabel: _t(\"Wavy 13\") },\n                    \"html_builder/Wavy/31\": { selectLabel: _t(\"Wavy 14\") },\n                },\n            },\n            blockAndRainy: {\n                label: _t(\"Block & Rainy\"),\n                shapes: {\n                    \"html_builder/Blocks/02_001\": { selectLabel: _t(\"Blocks 01\") },\n                    \"html_builder/Rainy/01_001\": { selectLabel: _t(\"Rainy 01\"), animated: true },\n                    \"html_builder/Blocks/01_001\": { selectLabel: _t(\"Blocks 02\") },\n                    \"html_builder/Rainy/02_001\": { selectLabel: _t(\"Rainy 02\"), animated: true },\n                    \"html_builder/Rainy/06\": { selectLabel: _t(\"Rainy 03\") },\n                    \"html_builder/Blocks/04\": { selectLabel: _t(\"Blocks 03\") },\n                    \"html_builder/Rainy/07\": { selectLabel: _t(\"Rainy 04\") },\n                    \"html_builder/Rainy/10\": { selectLabel: _t(\"Rainy 05\"), animated: true },\n                    \"html_builder/Floats/10\": { selectLabel: _t(\"Rainy 06\"), animated: true },\n                    \"html_builder/Floats/11\": { selectLabel: _t(\"Rainy 07\"), animated: true },\n                    \"html_builder/Rainy/08_001\": { selectLabel: _t(\"Rainy 08\"), animated: true },\n                    \"html_builder/Rainy/09_001\": { selectLabel: _t(\"Rainy 09\") },\n                },\n            },\n            miscellaneous: {\n                label: _t(\"Miscellaneous\"),\n                shapes: {\n                    \"html_builder/Floats/01\": {\n                        selectLabel: _t(\"Miscellaneous 01\"),\n                        animated: true,\n                    },\n                    \"html_builder/Floats/02\": {\n                        selectLabel: _t(\"Miscellaneous 02\"),\n                        animated: true,\n                    },\n                    \"html_builder/Floats/05\": {\n                        selectLabel: _t(\"Miscellaneous 03\"),\n                        animated: true,\n                    },\n                    \"html_builder/Floats/08\": {\n                        selectLabel: _t(\"Miscellaneous 04\"),\n                        animated: true,\n                    },\n                    \"html_builder/Floats/09\": {\n                        selectLabel: _t(\"Miscellaneous 05\"),\n                        animated: true,\n                    },\n                    \"html_builder/Floats/13\": {\n                        selectLabel: _t(\"Miscellaneous 06\"),\n                        animated: true,\n                    },\n                    \"html_builder/Floats/14\": {\n                        selectLabel: _t(\"Miscellaneous 07\"),\n                        animated: true,\n                    },\n                    \"html_builder/Zigs/01_001\": {\n                        selectLabel: _t(\"Miscellaneous 08\"),\n                        animated: true,\n                    },\n                },\n            },\n        },\n    },\n};\n", "import { ANIMATE, before } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class BadgeOption extends BaseOptionComponent {\n    static template = \"html_builder.BadgeOption\";\n    static selector = \".s_badge\";\n}\n\nclass BadgeOptionPlugin extends Plugin {\n    static id = \"badgeOption\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_options: [withSequence(before(ANIMATE), BadgeOption)],\n        so_content_addition_selector: [\".s_badge\"],\n    };\n}\nregistry.category(\"builder-plugins\").add(BadgeOptionPlugin.id, BadgeOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class BaseVerticalAlignmentOption extends BaseOptionComponent {\n    static template = \"html_builder.VerticalAlignmentOption\";\n    level = 1;\n    justify = true;\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { BLOCK_ALIGN } from \"@html_builder/utils/option_sequence\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nclass BlockAlignmentOptionPlugin extends Plugin {\n    static id = \"blockAlignmentOption\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_options: [withSequence(BLOCK_ALIGN, BlockAlignmentOption)],\n    };\n}\n\nexport class BlockAlignmentOption extends BaseOptionComponent {\n    static template = \"html_builder.BlockAlignmentOption\";\n    static selector = \".s_alert, .s_blockquote, .s_text_highlight\";\n}\n\nregistry.category(\"builder-plugins\").add(BlockAlignmentOptionPlugin.id, BlockAlignmentOptionPlugin);\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\n\nexport class BorderConfigurator extends BaseOptionComponent {\n    static template = \"html_builder.BorderConfiguratorOption\";\n    static dependencies = [\"builderActions\"];\n    static props = {\n        label: { type: String },\n        direction: { type: String, optional: true },\n        withRoundCorner: { type: Boolean, optional: true },\n        withBSClass: { type: Boolean, optional: true },\n        action: { type: String, optional: true },\n    };\n    static defaultProps = {\n        withRoundCorner: true,\n        withBSClass: true, // TODO remove, and actually configure propertly in caller\n        action: \"styleAction\",\n    };\n\n    setup() {\n        super.setup();\n        this.state = useDomState((editingElement) => ({\n            hasBorder: this.hasBorder(editingElement),\n        }));\n    }\n    getStyleActionParam(param) {\n        const property = `border-${this.props.direction ? this.props.direction + \"-\" : \"\"}${param}`;\n        if (this.props.withBSClass && (param === \"width\" || param === \"radius\")) {\n            // grep: --box-border-width, --box-border-radius\n            return `--box-${property}`;\n        }\n        return property;\n    }\n    hasBorder(editingElement) {\n        const { getAction } = this.dependencies.builderActions;\n        const styleActionValue = getAction(\"styleAction\").getValue({\n            editingElement,\n            params: {\n                mainParam: this.getStyleActionParam(\"width\"),\n            },\n        });\n        const values = (styleActionValue || \"0\").match(/\\d+/g);\n        return values.some((value) => parseInt(value) > 0);\n    }\n}\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { BorderConfigurator } from \"@html_builder/plugins/border_configurator_option\";\nimport { ShadowOption } from \"@html_builder/plugins/shadow_option\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\n\nexport class CTABadgeOption extends BaseOptionComponent {\n    static template = \"html_builder.CTABadgeOption\";\n    static selector = \".s_cta_badge\";\n    static components = { BorderConfigurator, ShadowOption };\n}\n\nclass CTABadgeOptionPlugin extends Plugin {\n    static id = \"ctaBadgeOption\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_options: [CTABadgeOption],\n        so_content_addition_selector: [\".s_cta_badge\"],\n    };\n}\nregistry.category(\"builder-plugins\").add(CTABadgeOptionPlugin.id, CTABadgeOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\n\nclass DateTimeFieldPlugin extends Plugin {\n    static id = \"dateTimeField\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        content_not_editable_selectors: [\n            \"[data-oe-field][data-oe-type=date]\",\n            \"[data-oe-field][data-oe-type=datetime]\",\n        ],\n    };\n}\nregistry.category(\"builder-plugins\").add(DateTimeFieldPlugin.id, DateTimeFieldPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { getCSSVariableValue, getHtmlStyle } from \"@html_editor/utils/formatting\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Cache } from \"@web/core/utils/cache\";\nimport { loadCSS } from \"@web/core/assets\";\nimport { BuilderFontSizeSelector } from \"./font_size_selector\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\n/**\n * @typedef {string[]} fontCssVariables\n */\n\nexport class BuilderFontPlugin extends Plugin {\n    static id = \"builderFont\";\n    static shared = [\"getFontsCache\", \"getFontsData\"];\n    static dependencies = [\"toolbar\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        // Lists CSS variables that will be reset when a font is deleted if\n        // they refer to that font.\n        fontCssVariables: [\n            \"font\",\n            \"headings-font\",\n            \"h2-font\",\n            \"h3-font\",\n            \"h4-font\",\n            \"h5-font\",\n            \"h6-font\",\n            \"display-1-font\",\n            \"display-2-font\",\n            \"display-3-font\",\n            \"display-4-font\",\n            \"buttons-font\",\n        ],\n        font_items: [\n            ...[\n                { name: _t(\"Header 1 Display 2\"), tagName: \"h1\", extraClass: \"display-2\" },\n                { name: _t(\"Header 1 Display 3\"), tagName: \"h1\", extraClass: \"display-3\" },\n                { name: _t(\"Header 1 Display 4\"), tagName: \"h1\", extraClass: \"display-4\" },\n            ].map((item) => withSequence(15, item)),\n            withSequence(43, { name: _t(\"Light\"), tagName: \"p\", extraClass: \"lead\" }),\n            withSequence(46, { name: _t(\"Small\"), tagName: \"p\", extraClass: \"small\" }),\n        ],\n    };\n    setup() {\n        this.fontsCache = new Cache(this._fetchFonts.bind(this), JSON.stringify);\n        const buttonGroups = this.dependencies.toolbar.getToolbarInfo().buttonGroups;\n        for (const buttonGroup of buttonGroups) {\n            if (buttonGroup.id !== \"font\") {\n                continue;\n            }\n            for (const button of buttonGroup.buttons) {\n                if (button.id === \"font-size\") {\n                    button.Component = BuilderFontSizeSelector;\n                }\n            }\n        }\n    }\n    destroy() {\n        super.destroy();\n        this.fontsCache.invalidate();\n    }\n    getFontsCache() {\n        return this.fontsCache;\n    }\n    async getFontsData() {\n        return this.fontsCache.read({});\n    }\n    async _fetchFonts() {\n        const style = getHtmlStyle(this.document);\n        const nbFonts = parseInt(getCSSVariableValue(\"number-of-fonts\", style));\n        // User fonts served by google server.\n        const googleFontsProperty = getCSSVariableValue(\"google-fonts\", style);\n        let googleFonts = googleFontsProperty ? googleFontsProperty.split(/\\s*,\\s*/g) : [];\n        googleFonts = googleFonts.map((font) => font.substring(1, font.length - 1)); // Unquote\n        // Local user fonts.\n        const googleLocalFontsProperty = getCSSVariableValue(\"google-local-fonts\", style);\n        const googleLocalFonts = googleLocalFontsProperty\n            ? googleLocalFontsProperty.slice(1, -1).split(/\\s*,\\s*/g)\n            : [];\n        const uploadedLocalFontsProperty = getCSSVariableValue(\"uploaded-local-fonts\", style);\n        const uploadedLocalFonts = uploadedLocalFontsProperty\n            ? uploadedLocalFontsProperty.slice(1, -1).split(/\\s*,\\s*/g)\n            : [];\n        // If a same font exists both remotely and locally, we remove the remote\n        // font to prioritize the local font. The remote one will never be\n        // displayed or loaded as long as the local one exists.\n        googleFonts = googleFonts.filter((font) => {\n            const localFonts = googleLocalFonts.map((localFont) => localFont.split(\":\")[0]);\n            return localFonts.indexOf(`'${font}'`) === -1;\n        });\n        const allFonts = [];\n\n        const fontsToLoad = [];\n        for (const font of googleFonts) {\n            const fontURL = `https://fonts.googleapis.com/css?family=${encodeURIComponent(\n                font\n            ).replace(/%20/g, \"+\")}:300,300i,400,400i,700,700i`;\n            fontsToLoad.push(fontURL);\n        }\n        for (const font of googleLocalFonts) {\n            const attachmentId = font.split(/\\s*:\\s*/)[1];\n            const fontURL = `/web/content/${encodeURIComponent(attachmentId)}`;\n            fontsToLoad.push(fontURL);\n        }\n        const proms = fontsToLoad.map(async (fontURL) => loadCSS(fontURL));\n\n        const _fonts = [];\n        const themeFontsNb =\n            nbFonts - (googleLocalFonts.length + googleFonts.length + uploadedLocalFonts.length);\n        const localFontsOffset = nbFonts - googleLocalFonts.length - uploadedLocalFonts.length;\n        const uploadedFontsOffset = nbFonts - uploadedLocalFonts.length;\n\n        for (let fontNb = 0; fontNb < nbFonts; fontNb++) {\n            const realFontNb = fontNb + 1;\n            const fontKey = getCSSVariableValue(`font-number-${realFontNb}`, style);\n            allFonts.push(fontKey);\n            let fontName = fontKey.slice(1, -1); // Unquote\n            const fontFamilyValue = `'${fontName}'`;\n            let styleFontFamily = fontName;\n            const isSystemFonts = fontName === \"SYSTEM_FONTS\";\n            if (isSystemFonts) {\n                fontName = _t(\"System Fonts\");\n                styleFontFamily = \"var(--o-system-fonts)\";\n            }\n\n            let type = isSystemFonts ? \"system\" : \"cloud\";\n            let indexForType = fontNb - themeFontsNb;\n            if (fontNb >= localFontsOffset) {\n                if (fontNb < uploadedFontsOffset) {\n                    type = \"google\";\n                    indexForType = fontNb - localFontsOffset;\n                } else {\n                    type = \"uploaded\";\n                    indexForType = fontNb - uploadedFontsOffset;\n                }\n            }\n            _fonts.push({\n                type,\n                indexForType,\n                fontFamilyValue,\n                styleFontFamily,\n                string: fontName,\n            });\n        }\n        await Promise.all(proms);\n        return {\n            allFonts,\n            googleFonts,\n            googleLocalFonts,\n            uploadedLocalFonts,\n            _fonts,\n        };\n    }\n}\nregistry.category(\"builder-plugins\").add(BuilderFontPlugin.id, BuilderFontPlugin);\n", "import { FontSizeSelector } from \"@html_editor/main/font/font_size_selector\";\n\nexport class BuilderFontSizeSelector extends FontSizeSelector {\n    static template = \"html_builder.FontSizeSelector\";\n    setup() {\n        super.setup();\n        this.fontSizeTags = {\n            \"display-1-font-size\": \"Display 1\",\n            \"display-2-font-size\": \"Display 2\",\n            \"display-3-font-size\": \"Display 3\",\n            \"display-4-font-size\": \"Display 4\",\n            \"h1-font-size\": \"Heading 1\",\n            \"h2-font-size\": \"Heading 2\",\n            \"h3-font-size\": \"Heading 3\",\n            \"h4-font-size\": \"Heading 4\",\n            \"h5-font-size\": \"Heading 5\",\n            \"h6-font-size\": \"Normal\",\n            \"font-size-base\": \"Normal\",\n            \"small-font-size\": \"Small\",\n        };\n    }\n}\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { shouldPreventGifTransformation } from \"@html_editor/main/media/image_post_process_plugin\";\nimport { loadImageInfo, isWebGLEnabled } from \"@html_editor/utils/image_processing\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ImageFilterOption extends BaseOptionComponent {\n    static template = \"html_builder.ImageFilterOption\";\n    static props = {\n        level: { type: Number, optional: true },\n    };\n    static defaultProps = {\n        level: 0,\n    };\n    setup() {\n        super.setup();\n        this.state = useDomState(async (editingElement) => {\n            const data = await loadImageInfo(editingElement).then((data) => ({\n                ...editingElement.dataset,\n                ...data,\n            }));\n            const canUseGlFilter = isWebGLEnabled();\n            return {\n                isCustomFilter: editingElement.dataset.glFilter === \"custom\",\n                showFilter: data.mimetypeBeforeConversion && !shouldPreventGifTransformation(data),\n                disableFilter: !canUseGlFilter,\n                tooltip: !canUseGlFilter ? _t(\"WebGL is not enabled on your browser.\") : undefined,\n            };\n        });\n    }\n}\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { normalizeColor } from \"@html_builder/utils/utils_css\";\nimport { defaultImageFilterOptions } from \"@html_editor/main/media/image_post_process_plugin\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { getHtmlStyle } from \"@html_editor/utils/formatting\";\nimport { registry } from \"@web/core/registry\";\n\nclass ImageFilterOptionPlugin extends Plugin {\n    static id = \"ImageFilterOption\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_actions: {\n            GlFilterAction,\n            SetCustomFilterAction,\n        },\n    };\n}\n\nexport class GlFilterAction extends BuilderAction {\n    static id = \"glFilter\";\n    static dependencies = [\"imagePostProcess\"];\n    isApplied({ editingElement, params: { mainParam: glFilterName } }) {\n        if (glFilterName) {\n            return editingElement.dataset.glFilter === glFilterName;\n        } else {\n            return !editingElement.dataset.glFilter;\n        }\n    }\n    async load({ editingElement: img, params: { mainParam: glFilterName } }) {\n        return await this.dependencies.imagePostProcess.processImage({\n            img,\n            newDataset: {\n                glFilter: glFilterName,\n            },\n        });\n    }\n    apply({ loadResult: updateImageAttributes }) {\n        updateImageAttributes();\n    }\n}\nexport class SetCustomFilterAction extends BuilderAction {\n    static id = \"setCustomFilter\";\n    static dependencies = [\"imagePostProcess\"];\n    getValue({ editingElement, params: { mainParam: filterProperty } }) {\n        const filterOptions = JSON.parse(editingElement.dataset.filterOptions || \"{}\");\n        return filterOptions[filterProperty] || defaultImageFilterOptions[filterProperty];\n    }\n    isApplied({ editingElement, params: { mainParam: filterProperty }, value: filterValue }) {\n        const filterOptions = JSON.parse(editingElement.dataset.filterOptions || \"{}\");\n        return (\n            filterValue ===\n            (filterOptions[filterProperty] || defaultImageFilterOptions[filterProperty])\n        );\n    }\n    async load({ editingElement: img, params: { mainParam: filterProperty }, value }) {\n        const filterOptions = JSON.parse(img.dataset.filterOptions || \"{}\");\n        filterOptions[filterProperty] =\n            filterProperty === \"filterColor\"\n                ? normalizeColor(value, getHtmlStyle(this.document))\n                : value;\n        return this.dependencies.imagePostProcess.processImage({\n            img,\n            newDataset: {\n                filterOptions: JSON.stringify(filterOptions),\n            },\n        });\n    }\n    apply({ loadResult: updateImageAttributes }) {\n        updateImageAttributes();\n    }\n}\n\nregistry.category(\"builder-plugins\").add(ImageFilterOptionPlugin.id, ImageFilterOptionPlugin);\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { getImageSrc, getMimetype } from \"@html_editor/utils/image\";\nimport { clamp } from \"@web/core/utils/numbers\";\n\nexport class ImageFormatOption extends BaseOptionComponent {\n    static template = \"html_builder.ImageFormat\";\n    static dependencies = [\"imageFormatOption\"];\n    static props = {\n        level: { type: Number, optional: true },\n        computeMaxDisplayWidth: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        level: 0,\n    };\n    MAX_SUGGESTED_WIDTH = 1920;\n    setup() {\n        super.setup();\n        const { computeAvailableFormats } = this.dependencies.imageFormatOption;\n        this.state = useDomState(async (editingElement) => {\n            const formats = await computeAvailableFormats(\n                editingElement,\n                this.computeMaxDisplayWidth.bind(this)\n            );\n            const hasSrc = !!getImageSrc(editingElement);\n            const mimetype = getMimetype(editingElement);\n            const compressionUnsupported =\n                mimetype === \"image/webp\" && this.webpCompressionUnuspported();\n            return {\n                showQuality: [\"image/jpeg\", \"image/webp\"].includes(mimetype),\n                compressionUnsupported: compressionUnsupported,\n                formats: hasSrc ? formats : [],\n            };\n        });\n    }\n    computeMaxDisplayWidth(img) {\n        if (this.props.computeMaxDisplayWidth) {\n            return this.props.computeMaxDisplayWidth(img);\n        }\n        return computeMaxDisplayWidth(img, this.MAX_SUGGESTED_WIDTH);\n    }\n    webpCompressionUnuspported() {\n        const canvas = document.createElement(\"canvas\");\n        canvas.width = canvas.height = 1;\n        return canvas.toDataURL(\"image/webp\").slice(0, 16) !== \"data:image/webp;\";\n    }\n}\n\nexport function computeMaxDisplayWidth(img, MAX_SUGGESTED_WIDTH = 1920) {\n    const window = img.ownerDocument.defaultView;\n    if (!window) {\n        return;\n    }\n    const computedStyles = window.getComputedStyle(img);\n    const displayWidth = parseFloat(computedStyles.getPropertyValue(\"width\"));\n    const gutterWidth = parseFloat(computedStyles.getPropertyValue(\"--o-grid-gutter-width\")) || 30;\n\n    // For the logos we don't want to suggest a width too small.\n    if (img.closest(\"nav\")) {\n        return Math.round(Math.min(displayWidth * 3, MAX_SUGGESTED_WIDTH));\n        // If the image is in a container(-small), it might get bigger on\n        // smaller screens. So we suggest the width of the current image unless\n        // it is smaller than the size of the container on the md breapoint\n        // (which is where our bootstrap columns fallback to full container\n        // width since we only use col-lg-* in Odoo).\n    } else if (img.closest(\".container, .o_container_small\")) {\n        const mdContainerMaxWidth =\n            parseFloat(computedStyles.getPropertyValue(\"--o-md-container-max-width\")) || 720;\n        const mdContainerInnerWidth = mdContainerMaxWidth - gutterWidth;\n        return Math.round(clamp(displayWidth, mdContainerInnerWidth, MAX_SUGGESTED_WIDTH));\n        // If the image is displayed in a container-fluid, it might also get\n        // bigger on smaller screens. The same way, we suggest the width of the\n        // current image unless it is smaller than the max size of the container\n        // on the md breakpoint (which is the LG breakpoint since the container\n        // fluid is full-width).\n    } else if (img.closest(\".container-fluid\")) {\n        const lgBp = parseFloat(computedStyles.getPropertyValue(\"--breakpoint-lg\")) || 992;\n        const mdContainerFluidMaxInnerWidth = lgBp - gutterWidth;\n        return Math.round(clamp(displayWidth, mdContainerFluidMaxInnerWidth, MAX_SUGGESTED_WIDTH));\n    }\n    // If it's not in a container, it's probably not going to change size\n    // depending on breakpoints. We still keep a margin safety.\n    return Math.round(Math.min(displayWidth * 1.5, MAX_SUGGESTED_WIDTH));\n}\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport {\n    DEFAULT_IMAGE_QUALITY,\n    shouldPreventGifTransformation,\n} from \"@html_editor/main/media/image_post_process_plugin\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { loadImage, loadImageInfo } from \"@html_editor/utils/image_processing\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { selectElements } from \"@html_editor/utils/dom_traversal\";\n\nclass ImageFormatOptionPlugin extends Plugin {\n    static id = \"imageFormatOption\";\n    static shared = [\"computeAvailableFormats\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_actions: {\n            SetImageFormatAction,\n            SetImageQualityAction,\n        },\n        on_snippet_dropped_handlers: async ({ snippetEl }) => {\n            for (const imgEl of selectElements(\n                snippetEl,\n                \"img:not([data-mimetype]), .oe_img_bg:not([data-mimetype])\"\n            )) {\n                const info = await loadImageInfo(imgEl);\n                imgEl.dataset.mimetype = info.mimetypeBeforeConversion;\n            }\n        },\n    };\n    /**\n     * Returns a list of valid formats for a given image or an empty list if\n     * there is no mimetypeBeforeConversion data attribute on the image.\n     */\n    async computeAvailableFormats(img, computeMaxDisplayWidth) {\n        const data = { ...img.dataset, ...(await loadImageInfo(img)) };\n        if (!data.mimetypeBeforeConversion || shouldPreventGifTransformation(data)) {\n            return [];\n        }\n\n        const maxWidth = await this.getImageWidth(data.originalSrc, data.width);\n        const optimizedWidth = Math.min(maxWidth, computeMaxDisplayWidth?.(img) || 0);\n        const widths = {\n            128: [\"128px\", this.config.defaultImageMimetype ?? \"image/webp\"],\n            256: [\"256px\", this.config.defaultImageMimetype ?? \"image/webp\"],\n            512: [\"512px\", this.config.defaultImageMimetype ?? \"image/webp\"],\n            1024: [\"1024px\", this.config.defaultImageMimetype ?? \"image/webp\"],\n            1920: [\"1920px\", this.config.defaultImageMimetype ?? \"image/webp\"],\n        };\n        widths[img.naturalWidth] = [\n            _t(\"%spx\", img.naturalWidth),\n            this.config.defaultImageMimetype ?? \"image/webp\",\n        ];\n        widths[optimizedWidth] = [\n            _t(\"%spx (Suggested)\", optimizedWidth),\n            this.config.defaultImageMimetype ?? \"image/webp\",\n        ];\n        const mimetypeBeforeConversion = data.mimetypeBeforeConversion;\n        widths[maxWidth] = [_t(\"%spx (Original)\", maxWidth), mimetypeBeforeConversion, true];\n        if (mimetypeBeforeConversion !== (this.config.defaultImageMimetype ?? \"image/webp\")) {\n            // Avoid a key collision by subtracting 0.1 - putting the default image mimetype\n            // above the original format one of the same size.\n            widths[maxWidth - 0.1] = [\n                _t(\"%spx\", maxWidth),\n                this.config.defaultImageMimetype ?? \"image/webp\",\n            ];\n        }\n        return Object.entries(widths)\n            .filter(([width]) => width <= maxWidth)\n            .sort(([v1], [v2]) => v1 - v2)\n            .map(([width, [label, mimetype, isOriginal]]) => {\n                const id = `${width}-${mimetype}`;\n                return { id, width: Math.round(width), label, mimetype, isOriginal };\n            });\n    }\n    async getImageWidth(originalSrc, width) {\n        const getNaturalWidth = () => loadImage(originalSrc).then((i) => i.naturalWidth);\n        return width ? Math.round(width) : await getNaturalWidth();\n    }\n}\n\nexport class SetImageFormatAction extends BuilderAction {\n    static id = \"setImageFormat\";\n    static dependencies = [\"imagePostProcess\"];\n    isApplied({ editingElement, params: { width, mimetype, isOriginal } }) {\n        const isOriginalUntouched =\n            (!editingElement.dataset.resizeWidth || !editingElement.dataset.formatMimetype) &&\n            isOriginal;\n        return (\n            isOriginalUntouched ||\n            (editingElement.dataset.resizeWidth === String(width) &&\n                editingElement.dataset.formatMimetype === mimetype)\n        );\n    }\n    async load({ editingElement: img, params: { width, mimetype } }) {\n        return this.dependencies.imagePostProcess.processImage({\n            img,\n            newDataset: {\n                resizeWidth: width,\n                formatMimetype: mimetype,\n            },\n        });\n    }\n    apply({ loadResult: updateImageAttributes }) {\n        updateImageAttributes();\n    }\n}\nexport class SetImageQualityAction extends BuilderAction {\n    static id = \"setImageQuality\";\n    static dependencies = [\"imagePostProcess\"];\n    getValue({ editingElement: img }) {\n        return (\"quality\" in img.dataset && img.dataset.quality) || DEFAULT_IMAGE_QUALITY;\n    }\n    async load({ editingElement: img, value: quality }) {\n        return this.dependencies.imagePostProcess.processImage({\n            img,\n            newDataset: {\n                quality,\n            },\n        });\n    }\n    apply({ loadResult: updateImageAttributes }) {\n        updateImageAttributes();\n    }\n}\n\nregistry.category(\"builder-plugins\").add(ImageFormatOptionPlugin.id, ImageFormatOptionPlugin);\n", "export function getShapeURL(shapeName) {\n    const [module, directory, fileName] = shapeName.split(\"/\");\n    return `/${encodeURIComponent(module)}/static/image_shapes/${encodeURIComponent(\n        directory\n    )}/${encodeURIComponent(fileName)}.svg`;\n}\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { toRatio } from \"@html_builder/utils/utils\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ShapeSelector } from \"@html_builder/plugins/shape/shape_selector\";\nimport { deepCopy } from \"@web/core/utils/objects\";\n\nexport class ImageShapeOption extends BaseOptionComponent {\n    static template = \"html_builder.ImageShapeOption\";\n    static dependencies = [\"customizeTab\", \"imageShapeOption\"];\n    static props = {\n        withAnimatedShapes: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        withAnimatedShapes: true,\n    };\n    setup() {\n        super.setup();\n        this.customizeTabPlugin = this.dependencies.customizeTab;\n        this.imageShapeOption = this.dependencies.imageShapeOption;\n        this.toRatio = toRatio;\n        this.state = useDomState((editingElement) => {\n            const shape = editingElement.dataset.shape;\n            return {\n                hasShape: !!shape && !this.imageShapeOption.isTechnicalShape(shape),\n                shapeLabel: this.imageShapeOption.getShapeLabel(shape),\n                showImageShape0: this.isShapeVisible(editingElement, 0),\n                showImageShape1: this.isShapeVisible(editingElement, 1),\n                showImageShape2: this.isShapeVisible(editingElement, 2),\n                showImageShape3: this.isShapeVisible(editingElement, 3),\n                showImageShape4: this.isShapeVisible(editingElement, 4),\n                showImageShapeTransform: this.imageShapeOption.isTransformableShape(shape),\n                showImageShapeAnimation: this.imageShapeOption.isAnimableShape(shape),\n                togglableRatio: this.imageShapeOption.isTogglableRatioShape(shape),\n            };\n        });\n    }\n    getFilteredGroups() {\n        if (this.props.withAnimatedShapes) {\n            return this.imageShapeOption.getImageShapeGroups();\n        }\n        const allDefinitions = deepCopy(this.imageShapeOption.getImageShapeGroups());\n        for (const [dName, definition] of Object.entries(allDefinitions)) {\n            for (const [gName, subgroup] of Object.entries(definition.subgroups)) {\n                for (const [sName, shape] of Object.entries(subgroup.shapes)) {\n                    if (shape.animated) {\n                        delete subgroup.shapes[sName];\n                    }\n                }\n                if (Object.keys(subgroup.shapes).length === 0) {\n                    delete definition.subgroups[gName];\n                }\n            }\n            if (Object.keys(definition.subgroups).length === 0) {\n                delete allDefinitions[dName];\n            }\n        }\n        return allDefinitions;\n    }\n    isShapeVisible(img, shapeIndex) {\n        const shapeName = img.dataset.shape;\n        const shapeColors = img.dataset.shapeColors;\n        if (!shapeName || !shapeColors) {\n            return false;\n        }\n        const colors = img.dataset.shapeColors.split(\";\");\n        return colors[shapeIndex];\n    }\n    showImageShapes() {\n        this.customizeTabPlugin.openCustomizeComponent(\n            ShapeSelector,\n            this.env.getEditingElements(),\n            {\n                shapeActionId: \"setImageShape\",\n                buttonWrapperClassName: \"o-hb-img-shape-btn\",\n                selectorTitle: _t(\"Shapes\"),\n                shapeGroups: this.getFilteredGroups(),\n            }\n        );\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { DEFAULT_PALETTE } from \"@html_editor/utils/color\";\nimport { getShapeURL } from \"@html_builder/plugins/image/image_helpers\";\nimport {\n    activateCropper,\n    createDataURL,\n    cropperDataFields,\n    loadImage,\n    loadImageInfo,\n    isGif,\n} from \"@html_editor/utils/image_processing\";\nimport { getValueFromVar } from \"@html_builder/utils/utils\";\nimport { imageShapeDefinitions } from \"@html_builder/plugins/image/image_shapes_definition\";\nimport {\n    getImageTransformationData,\n    shouldPreventGifTransformation,\n} from \"@html_editor/main/media/image_post_process_plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { getMimetype } from \"@html_editor/utils/image\";\n\n/**\n * @typedef {((dataset: DOMStringMap) => string)[]} default_shape_handlers\n * @typedef {((\n *     svg: SVGElement,\n *     params: {\n *         shapeId: string,\n *         shapeFlip: \"x\" | \"y\",\n *         shapeRotate: 0 | \"90\" | \"180\" | \"270\",\n *         shapeAnimationSpeed: number,\n *         shapeColors: string,\n *     }\n * ) => Promise<void>)[]} post_compute_shape_listeners\n */\n\n// Regex definitions to apply speed modification in SVG files\n// Note : These regex patterns are duplicated on the server side for\n// background images that are part of a CSS rule \"background-image: ...\". The\n// client-side regex patterns are used for images that are part of an\n// \"src\" attribute with a base64 encoded svg in the <img> tag. Perhaps we should\n// consider finding a solution to define them only once? The issue is that the\n// regex patterns in Python are slightly different from those in JavaScript.\n// See : controllers/main.py\nconst CSS_ANIMATION_RULE_REGEX =\n    /(?<declaration>animation(?:-duration)?:\\s*.*?)(?<value>(?:\\d+(?:\\.\\d+)?)|(?:\\.\\d+))(?<unit>ms|s)(?<separator>\\s|;|\"|$)/gm;\nconst SVG_DUR_TIMECOUNT_VAL_REGEX =\n    /(?<attribute_name>\\sdur=\"\\s*)(?<value>(?:\\d+(?:\\.\\d+)?)|(?:\\.\\d+))(?<unit>h|min|ms|s)?\\s*\"/gm;\nconst CSS_ANIMATION_RATIO_REGEX = /(--animation_ratio: (?<ratio>\\d*(\\.\\d+)?));/m;\n\nexport class ImageShapeOptionPlugin extends Plugin {\n    static id = \"imageShapeOption\";\n    static dependencies = [\"history\", \"userCommand\", \"imagePostProcess\", \"imageToolOption\"];\n    static shared = [\n        \"getImageShapeGroups\",\n        \"isTransformableShape\",\n        \"isTechnicalShape\",\n        \"isAnimableShape\",\n        \"isTogglableRatioShape\",\n        \"getShapeLabel\",\n        \"loadShape\",\n    ];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_actions: {\n            SetImageShapeAction,\n            SetImgShapeColorAction,\n            FlipImageShapeAction,\n            RotateImageShapeAction,\n            SetImageShapeSpeedAction,\n            ToggleImageShapeRatioAction,\n        },\n        process_image_warmup_handlers: this.processImageWarmup.bind(this),\n        process_image_post_handlers: this.processImagePost.bind(this),\n        hover_effect_allowed_predicates: (el) => this.canHaveHoverEffect(el),\n    };\n    setup() {\n        this.shapeSvgTextCache = {};\n        this.imageShapes = this.makeImageShapes();\n        // Compatibility with old shapes.\n        for (const shapeId of Object.keys(this.imageShapes)) {\n            const oldShapeId = shapeId.replace(\"html_builder\", \"web_editor\");\n            this.imageShapes[oldShapeId] = this.imageShapes[shapeId];\n        }\n    }\n    async canHaveHoverEffect(imgEl) {\n        const dataset = Object.assign({}, imgEl.dataset, await loadImageInfo(imgEl));\n        return (\n            imgEl.tagName === \"IMG\" &&\n            !this.isDeviceShape(imgEl) &&\n            !this.isAnimableShape(dataset.shape) &&\n            this.isImageSupportedForShapes(imgEl, dataset)\n        );\n    }\n    isDeviceShape(img) {\n        const shapeName = img.dataset.shape;\n        if (!shapeName) {\n            return false;\n        }\n        const shapeCategory = shapeName.split(\"/\")[1];\n        return shapeCategory === \"devices\";\n    }\n    isImageSupportedForShapes(img, dataset = img.dataset) {\n        // todo: The hover effect and shape code should probably be define somewhere else.\n        if (!!dataset.hoverEffect || !!dataset.shape) {\n            return true;\n        }\n        if (!dataset.originalId) {\n            return false;\n        }\n        return isImageSupportedForProcessing(getMimetype(img, dataset));\n    }\n    async getShapeSvgText(shapeName) {\n        // Compatibility with old shapes.\n        const shape = shapeName.replace(\"web_editor\", \"html_builder\");\n        let shapeSvgText = this.shapeSvgTextCache[shape];\n        if (shapeSvgText) {\n            return shapeSvgText;\n        }\n        const shapeURL = getShapeURL(shape);\n        shapeSvgText = await (await fetch(shapeURL)).text();\n        this.shapeSvgTextCache[shape] = shapeSvgText;\n        return shapeSvgText;\n    }\n    async loadShape(img, newData = {}) {\n        // todo: find a way to apply to carousel thumbnail after processImage\n        return this.dependencies.imagePostProcess.processImage({ img, newDataset: newData });\n    }\n    async processImageWarmup(img, newDataset) {\n        const getData = (propName) =>\n            propName in newDataset ? newDataset[propName] : img.dataset[propName];\n        const combinedDataset = { ...img.dataset, ...newDataset };\n        const previousShapeId = this.getDefaultShapeId(img.dataset);\n        const shapeId = combinedDataset.shape || this.getDefaultShapeId(combinedDataset);\n        // todo: should we reset some data if shapeName is not defined?\n        if (!shapeId) {\n            return;\n        }\n        const isNewShape = previousShapeId !== shapeId;\n        const shapeSvgText = await this.getShapeSvgText(shapeId);\n\n        // Get colors.\n        const defaultShapeColors = this.getThemedSvgColors(shapeSvgText).join(\";\");\n        newDataset.shapeColors =\n            newDataset.shapeColors ??\n            (isNewShape ? defaultShapeColors : img.dataset.shapeColors ?? defaultShapeColors);\n\n        const getNaturalWidth = async () => {\n            if (img.naturalWidth) {\n                return img.naturalWidth;\n            }\n            const loadedImgEl = await loadImage(img.getAttribute(\"src\"));\n            return loadedImgEl.naturalWidth;\n        };\n        const svgWidth = getData(\"resizeWidth\") || getData(\"width\") || (await getNaturalWidth());\n\n        // Get the svg element.\n        const svg = await this.computeShape(shapeSvgText, {\n            ...img.dataset,\n            ...newDataset,\n            shapeId,\n            shapeFlip: getData(\"shapeFlip\") || \"\",\n            shapeRotate: getData(\"shapeRotate\") || 0,\n            shapeAnimationSpeed: Number(getData(\"shapeAnimationSpeed\")) || 0,\n            shapeColors: newDataset.shapeColors,\n        });\n\n        const svgAspectRatio =\n            parseInt(svg.getAttribute(\"width\")) / parseInt(svg.getAttribute(\"height\"));\n        const imgAspectRatio = svg.dataset.imgAspectRatio;\n\n        if (isNewShape && !(\"aspectRatio\" in newDataset)) {\n            const data = getImageTransformationData({ ...img.dataset, ...newDataset });\n\n            // The togglable ratio is squared by default.\n            const shouldBeSquared =\n                this.imageShapes[shapeId].togglableRatio && !img.dataset.aspectRatio;\n            if (shouldBeSquared && !shouldPreventGifTransformation(data)) {\n                newDataset.aspectRatio = \"1/1\";\n            }\n        }\n\n        /**\n         * @param {HTMLCanvasElement} canvas\n         * @param {Object} data dataset containing the cropperDataFields\n         */\n        const postProcessCroppedCanvas = async (canvas) => {\n            const img = await loadImage(canvas.toDataURL());\n            document.createElement(\"div\").appendChild(img);\n            const cropper = await activateCropper(img, 1, { y: 0 });\n            const croppedCanvas = cropper.getCroppedCanvas();\n            cropper.destroy();\n            return croppedCanvas;\n        };\n\n        return {\n            getHeight: svg.dataset.imgPerspective && ((canvas) => canvas.width / svgAspectRatio),\n            perspective: svg.dataset.imgPerspective || null,\n            newDataset,\n            // If imgAspectRatio is defined, the image is cropped a second time\n            // after the first crop to ensure that the ratio of the shape and the\n            // image are the same.\n            postProcessCroppedCanvas: imgAspectRatio && postProcessCroppedCanvas,\n\n            svg,\n            svgAspectRatio,\n            svgWidth,\n        };\n    }\n    async processImagePost(b64url, handlerDataset, processContext) {\n        const { svg, svgAspectRatio, svgWidth } = processContext;\n        if (!svg) {\n            return;\n        }\n        svg.querySelectorAll(\"image\").forEach((image) => {\n            image.setAttribute(\"xlink:href\", b64url);\n        });\n        // Force natural width & height (note: loading the original image is\n        // needed for Safari where natural width & height of SVG does not return\n        // the correct values).\n        const loadedImage = await loadImage(b64url);\n        // If the svg forces the size of the shape we still want to have the resized\n        // width\n        if (!svg.dataset.forcedSize) {\n            svg.setAttribute(\"width\", loadedImage.naturalWidth);\n            svg.setAttribute(\"height\", loadedImage.naturalHeight);\n        } else {\n            const imageWidth = Math.trunc(svgWidth);\n            const newHeight = imageWidth / svgAspectRatio;\n            svg.setAttribute(\"width\", imageWidth);\n            svg.setAttribute(\"height\", newHeight);\n        }\n\n        // Transform the current SVG in a base64 file to be saved by the server\n        const blob = new Blob([svg.outerHTML], {\n            type: \"image/svg+xml\",\n        });\n        const dataURL = await createDataURL(blob);\n        return [dataURL, { ...handlerDataset, mimetype: \"image/svg+xml\" }];\n    }\n\n    /**\n     * Sets the image in the supplied SVG and replace the src with a dataURL\n     *\n     * @param {string} svgText svg text file\n     * @param {HTMLImageElement} img\n     * @returns {SVGElement}\n     */\n    async computeShape(svgText, params) {\n        const { shapeId, shapeFlip, shapeRotate, shapeAnimationSpeed, shapeColors } = params;\n        // Apply the colors to the shape.\n        svgText = this.replaceSvgColors(svgText, shapeColors.split(\";\"));\n        // Apply the right animation speed if there is an animated shape.\n        if (shapeAnimationSpeed) {\n            svgText = this.replaceAnimationDuration(svgText, shapeAnimationSpeed);\n        }\n\n        const svg = new DOMParser().parseFromString(svgText, \"image/svg+xml\").documentElement;\n\n        // Modifies the SVG according to the \"flip\" or/and \"rotate\" options.\n        if ((shapeFlip || shapeRotate) && this.isTransformableShape(shapeId)) {\n            const shapeTransformValues = [];\n            if (shapeFlip) {\n                // Possible values => \"x\", \"y\", \"xy\"\n                shapeTransformValues.push(\n                    `scale${shapeFlip === \"x\" ? \"X\" : shapeFlip === \"y\" ? \"Y\" : \"\"}(-1)`\n                );\n            }\n            if (shapeRotate) {\n                // Possible values => \"90\", \"180\", \"270\"\n                shapeTransformValues.push(`rotate(${shapeRotate}deg)`);\n            }\n            // \"transform-origin: center;\" does not work on \"#filterPath\". But\n            // since its dimension is 1px * 1px the following solution works.\n            const transformOrigin = \"transform-origin: 0.5px 0.5px;\";\n            // Applies the transformation values to the path used to create a\n            // mask over the SVG image.\n            svg.querySelector(\"#filterPath\").setAttribute(\n                \"style\",\n                `transform: ${shapeTransformValues.join(\" \")}; ${transformOrigin}`\n            );\n        }\n\n        for (const cb of this.getResource(\"post_compute_shape_listeners\")) {\n            await cb(svg, params);\n        }\n\n        svg.removeChild(svg.querySelector(\"#preview\"));\n        return svg;\n    }\n    /**\n     * Replace animation durations in SVG and CSS with modified values.\n     *\n     * This function takes a ratio and an SVG string containing animations. It\n     * uses regular expressions to find and replace the duration values in both\n     * CSS animation rules and SVG duration attributes based on the provided\n     * ratio.\n     *\n     * @param {string} svgText The SVG string containing animations.\n     * @param {number} speed The speed used to calculate the new animation\n     *                       durations. If speed is 0.0, the original\n     *                       durations are preserved.\n     * @returns {string} The modified SVG string with updated animation\n     *                   durations.\n     */\n    replaceAnimationDuration(svgText, speed) {\n        const ratio = (speed >= 0.0 ? 1.0 + speed : 1.0 / (1.0 - speed)).toFixed(3);\n        // Callback for CSS 'animation' and 'animation-duration' declarations\n        function callbackCssAnimationRule(match, declaration, value, unit, separator) {\n            value = parseFloat(value) / (ratio ? ratio : 1);\n            return `${declaration}${value}${unit}${separator}`;\n        }\n\n        // Callback function for handling the 'dur' SVG attribute timecount\n        // value in accordance with the SMIL animation specification (e.g., 4s,\n        // 2ms). If no unit is provided, seconds are implied.\n        function callbackSvgDurTimecountVal(match, attribute_name, value, unit) {\n            value = parseFloat(value) / (ratio ? ratio : 1);\n            return `${attribute_name}${value}${unit ? unit : \"s\"}\"`;\n        }\n\n        // Applying regex substitutions to modify animation speed in the 'svg'\n        // variable.\n        svgText = svgText.replace(CSS_ANIMATION_RULE_REGEX, callbackCssAnimationRule);\n        svgText = svgText.replace(SVG_DUR_TIMECOUNT_VAL_REGEX, callbackSvgDurTimecountVal);\n        if (CSS_ANIMATION_RATIO_REGEX.test(svgText)) {\n            // Replace the CSS --animation_ratio variable for future purpose.\n            svgText = svgText.replace(CSS_ANIMATION_RATIO_REGEX, `--animation_ratio: ${ratio};`);\n        } else {\n            // Add the style tag with the root variable --animation ratio for\n            // future purpose.\n            const regex = /<svg .*>/m;\n            const subst = `$&\\n\\t<style>\\n\\t\\t:root { \\n\\t\\t\\t--animation_ratio: ${ratio};\\n\\t\\t}\\n\\t</style>`;\n            svgText = svgText.replace(regex, subst);\n        }\n        return svgText;\n    }\n\n    replaceSvgColors(shapeSvgText, colors) {\n        const svgColors = this.getSvgColors(shapeSvgText);\n        for (const [i, color] of colors.entries()) {\n            shapeSvgText = shapeSvgText.replace(\n                new RegExp(svgColors[i], \"g\"),\n                this.dependencies.imageToolOption.getCSSColorValue(color)\n            );\n        }\n        return shapeSvgText;\n    }\n    getSvgColors(shapeSvgText) {\n        // Map the default palette colors to an array if the shape includes them\n        // If they do not map a NULL, this way we know if a default color is in\n        // the shape\n        return Object.values(DEFAULT_PALETTE).map((color) =>\n            shapeSvgText.includes(color) ? color : null\n        );\n    }\n    getThemedSvgColors(shapeSvgText) {\n        const svgColors = this.getSvgColors(shapeSvgText);\n        return svgColors.map((color, i) =>\n            color !== null\n                ? this.dependencies.imageToolOption.getCSSColorValue(`o-color-${i + 1}`)\n                : null\n        );\n    }\n    applyShapeColors(editingElement, newColors) {}\n    isTransformableShape(shapeId) {\n        if (!shapeId) {\n            return false;\n        }\n        const canTransform = this.imageShapes[shapeId].transform;\n        return typeof canTransform === \"undefined\" ? true : canTransform;\n    }\n    isTechnicalShape(shapeId) {\n        if (!shapeId) {\n            return false;\n        }\n        return this.imageShapes[shapeId].isTechnical;\n    }\n    getShapeLabel(shapeId) {\n        if (!shapeId) {\n            return _t(\"None\");\n        }\n        return this.imageShapes[shapeId].selectLabel || _t(\"None\");\n    }\n    isAnimableShape(shape) {\n        if (!shape) {\n            return false;\n        }\n        return this.imageShapes[shape].animated;\n    }\n    isTogglableRatioShape(shape) {\n        if (!shape) {\n            return false;\n        }\n        return this.imageShapes[shape].togglableRatio;\n    }\n    getImageShapeGroups() {\n        return imageShapeDefinitions;\n    }\n    makeImageShapes() {\n        const entries = Object.values(this.getImageShapeGroups())\n            .map((x) =>\n                Object.values(x.subgroups)\n                    .map((x) => Object.entries(x.shapes))\n                    .flat()\n            )\n            .flat();\n        return Object.fromEntries(entries);\n    }\n    getDefaultShapeId(dataset) {\n        for (const fn of this.getResource(\"default_shape_handlers\")) {\n            const shapeId = fn(dataset);\n            if (shapeId) {\n                return shapeId;\n            }\n        }\n    }\n}\n\nexport class SetImageShapeAction extends BuilderAction {\n    static id = \"setImageShape\";\n    static dependencies = [\"imageShapeOption\"];\n    async load({ editingElement: img, value: shapeId }) {\n        const params = { shape: shapeId };\n        // A crop is applied to the image at the same time as certain shapes,\n        // which is why we reset the crop here or when the shape is removed.\n        // However, we don\u2019t reset it when the crop was applied intentionally.\n        // In that case, there are crop values; otherwise, there are none,\n        // only a 'data-aspect-ratio'.\n        if (\n            !shapeId &&\n            img.dataset.aspectRatio &&\n            !cropperDataFields.some((field) => field in img.dataset)\n        ) {\n            params[\"aspectRatio\"] = undefined;\n        }\n        // todo nby: re-read the old option method `setImgShape` and be sure all the logic is in there\n        return this.dependencies.imageShapeOption.loadShape(img, params);\n    }\n    apply({ editingElement: img, loadResult: updateImageAttributes }) {\n        updateImageAttributes();\n        const imgFilename = img.dataset.originalSrc.split(\"/\").pop().split(\".\")[0];\n        img.dataset.fileName = `${imgFilename}.svg`;\n    }\n    isApplied({ editingElement: img, value }) {\n        const datasetShape = img.dataset.shape;\n        if (!datasetShape) {\n            return false;\n        }\n        // Compatibility with old shapes.\n        const currentShape = datasetShape.replace(\"web_editor\", \"html_builder\");\n        return currentShape === value;\n    }\n}\nexport class SetImgShapeColorAction extends BuilderAction {\n    static id = \"setImgShapeColor\";\n    static dependencies = [\"imageShapeOption\", \"imageToolOption\"];\n    getValue({ editingElement: img, params: { index: colorIndex } }) {\n        return img.dataset.shapeColors?.split(\";\")[colorIndex] || \"\";\n    }\n    async load({ editingElement: img, params: { index: colorIndex }, value: color }) {\n        color = getValueFromVar(color);\n        const newColorId = parseInt(colorIndex);\n        const oldColors = img.dataset.shapeColors.split(\";\");\n        const newColors = oldColors.slice(0);\n        newColors[newColorId] = this.dependencies.imageToolOption.getCSSColorValue(\n            color === \"\" ? `o-color-${newColorId + 1}` : color\n        );\n        return this.dependencies.imageShapeOption.loadShape(img, {\n            shapeColors: newColors.join(\";\"),\n        });\n    }\n    apply({ loadResult: updateImageAttributes }) {\n        updateImageAttributes();\n    }\n}\nexport class FlipImageShapeAction extends BuilderAction {\n    static id = \"flipImageShape\";\n    static dependencies = [\"imageShapeOption\"];\n    async load({ editingElement: img, params: { axis } }) {\n        const currentAxis = img.dataset.shapeFlip || \"\";\n        const newAxis = currentAxis.includes(axis)\n            ? currentAxis.replace(axis, \"\")\n            : currentAxis + axis;\n        return this.dependencies.imageShapeOption.loadShape(img, {\n            shapeFlip: newAxis === \"yx\" ? \"xy\" : newAxis,\n        });\n    }\n    apply({ loadResult: updateImageAttributes }) {\n        updateImageAttributes();\n    }\n}\n\nexport class RotateImageShapeAction extends BuilderAction {\n    static id = \"rotateImageShape\";\n    static dependencies = [\"imageShapeOption\"];\n    async load({ editingElement: img, params: { side } }) {\n        const currentRotateValue = parseInt(img.dataset.shapeRotate) || 0;\n        const rotation = side === \"left\" ? -90 : 90;\n        const newRotateValue = (currentRotateValue + rotation + 360) % 360;\n        return this.dependencies.imageShapeOption.loadShape(img, { shapeRotate: newRotateValue });\n    }\n    apply({ loadResult: updateImageAttributes }) {\n        updateImageAttributes();\n    }\n}\nexport class SetImageShapeSpeedAction extends BuilderAction {\n    static id = \"setImageShapeSpeed\";\n    static dependencies = [\"imageShapeOption\"];\n    getValue({ editingElement: img }) {\n        return img.dataset.shapeAnimationSpeed || 0;\n    }\n    async load({ editingElement: img, value: speed }) {\n        return this.dependencies.imageShapeOption.loadShape(img, {\n            shapeAnimationSpeed: speed,\n        });\n    }\n    apply({ loadResult: updateImageAttributes }) {\n        updateImageAttributes();\n    }\n}\nexport class ToggleImageShapeRatioAction extends BuilderAction {\n    static id = \"toggleImageShapeRatio\";\n    static dependencies = [\"imageShapeOption\"];\n\n    isApplied({ editingElement: img }) {\n        return img.dataset.aspectRatio !== \"1/1\";\n    }\n    async load({ editingElement: img }) {\n        const isStretched = img.dataset.aspectRatio !== \"1/1\";\n        return this.dependencies.imageShapeOption.loadShape(img, {\n            aspectRatio: isStretched ? \"1/1\" : \"0/0\",\n            x: undefined,\n            y: undefined,\n            width: undefined,\n            height: undefined,\n        });\n    }\n    apply({ editingElement: img, loadResult: updateImageAttributes }) {\n        updateImageAttributes();\n    }\n}\n\nregistry.category(\"builder-plugins\").add(ImageShapeOptionPlugin.id, ImageShapeOptionPlugin);\n\n/**\n * @param {String} mimetype\n * @param {Boolean} [strict=false] if true, even partially supported images (GIFs)\n *     won't be accepted.\n * @returns {Boolean}\n */\nfunction isImageSupportedForProcessing(mimetype, strict = false) {\n    if (isGif(mimetype)) {\n        return !strict;\n    }\n    return [\"image/jpeg\", \"image/png\", \"image/webp\"].includes(mimetype);\n}\n", "import { _t } from \"@web/core/l10n/translation\";\n\nexport const imageShapeDefinitions = {\n    basic: {\n        label: _t(\"Basic\"),\n        subgroups: {\n            geometrics: {\n                label: _t(\"Geometrics\"),\n                shapes: {\n                    // todo: find it's proper place when implementing\n                    // hovering an image without shape.\n                    \"html_builder/geometric/geo_square\": {\n                        transform: false,\n                        isTechnical: true,\n                    },\n                    \"html_builder/geometric/geo_shuriken\": {\n                        selectLabel: _t(\"Shuriken\"),\n                        transform: false,\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_diamond\": {\n                        selectLabel: _t(\"Diamond\"),\n                        transform: false,\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_triangle\": {\n                        selectLabel: _t(\"Triangle\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_cornered_triangle\": {\n                        selectLabel: _t(\"Corner Triangle\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_pentagon\": {\n                        selectLabel: _t(\"Pentagon\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_hexagon\": {\n                        selectLabel: _t(\"Hexagon\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_heptagon\": {\n                        selectLabel: _t(\"Heptagon\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_star\": {\n                        selectLabel: _t(\"Star 1\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_star_8pin\": {\n                        selectLabel: _t(\"Star 2\"),\n                        transform: false,\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_star_16pin\": {\n                        selectLabel: _t(\"Star 3\"),\n                        transform: false,\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_slanted\": {\n                        selectLabel: _t(\"Slanted\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_emerald\": {\n                        selectLabel: _t(\"Emerald\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_tetris\": {\n                        selectLabel: _t(\"Tetris\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_kayak\": {\n                        selectLabel: _t(\"Kayak\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_tear\": {\n                        selectLabel: _t(\"Tear\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_gem\": {\n                        selectLabel: _t(\"Gem\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_sonar\": {\n                        selectLabel: _t(\"Sonar\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_door\": {\n                        selectLabel: _t(\"Door\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric/geo_square_1\": {\n                        selectLabel: _t(\"Square 1\"),\n                        animated: true,\n                    },\n                    \"html_builder/geometric/geo_square_2\": {\n                        selectLabel: _t(\"Square 2\"),\n                        animated: true,\n                    },\n                    \"html_builder/geometric/geo_square_3\": {\n                        selectLabel: _t(\"Square 3\"),\n                        animated: true,\n                    },\n                    \"html_builder/geometric/geo_square_4\": {\n                        selectLabel: _t(\"Square 4\"),\n                        animated: true,\n                    },\n                    \"html_builder/geometric/geo_square_5\": {\n                        selectLabel: _t(\"Square 5\"),\n                        animated: true,\n                    },\n                    \"html_builder/geometric/geo_square_6\": {\n                        selectLabel: _t(\"Square 6\"),\n                        animated: true,\n                    },\n                },\n            },\n            geometrics_rounded: {\n                label: _t(\"Geometrics Rounded\"),\n                shapes: {\n                    \"html_builder/geometric_round/geo_round_circle\": {\n                        selectLabel: _t(\"Circle\"),\n                        transform: false,\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_square\": {\n                        selectLabel: _t(\"Square (R)\"),\n                        transform: false,\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_diamond\": {\n                        selectLabel: _t(\"Diamond (R)\"),\n                        transform: false,\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_shuriken\": {\n                        selectLabel: _t(\"Shuriken (R)\"),\n                        transform: false,\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_triangle\": {\n                        selectLabel: _t(\"Triangle (R)\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_pentagon\": {\n                        selectLabel: _t(\"Pentagon (R)\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_hexagon\": {\n                        selectLabel: _t(\"Hexagon (R)\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_heptagon\": {\n                        selectLabel: _t(\"Heptagon (R)\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_star\": {\n                        selectLabel: _t(\"Star 1 (R)\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_star_7pin\": {\n                        selectLabel: _t(\"Star 2 (R)\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_star_8pin\": {\n                        selectLabel: _t(\"Star 3 (R)\"),\n                        transform: false,\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_star_16pin\": {\n                        selectLabel: _t(\"Star 4 (R)\"),\n                        transform: false,\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_emerald\": {\n                        selectLabel: _t(\"Emerald (R)\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_lemon\": {\n                        selectLabel: _t(\"Lemon (R)\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_tear\": {\n                        selectLabel: _t(\"Tear (R)\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_pill\": {\n                        selectLabel: _t(\"Pill (R)\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_gem\": {\n                        selectLabel: _t(\"Gem (R)\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_cornered\": {\n                        selectLabel: _t(\"Cornered\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_door\": {\n                        selectLabel: _t(\"Door (R)\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_sonar\": {\n                        selectLabel: _t(\"Sonar (R)\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_clover\": {\n                        selectLabel: _t(\"Clover (R)\"),\n                        transform: false,\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_bread\": {\n                        selectLabel: _t(\"Bread (R)\"),\n                        transform: false,\n                        togglableRatio: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_square_1\": {\n                        selectLabel: _t(\"Square 1 (R)\"),\n                        animated: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_square_2\": {\n                        selectLabel: _t(\"Square 2 (R)\"),\n                        animated: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_blob_soft\": {\n                        selectLabel: _t(\"Blob Soft\"),\n                        animated: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_blob_medium\": {\n                        selectLabel: _t(\"Blob Medium\"),\n                        animated: true,\n                    },\n                    \"html_builder/geometric_round/geo_round_blob_hard\": {\n                        selectLabel: _t(\"Blob Hard\"),\n                        animated: true,\n                    },\n                },\n            },\n            geometric_panels: {\n                label: _t(\"Geometrics Panels\"),\n                shapes: {\n                    \"html_builder/panel/panel_duo\": {\n                        selectLabel: _t(\"Duo\"),\n                    },\n                    \"html_builder/panel/panel_duo_r\": {\n                        selectLabel: _t(\"Duo (R)\"),\n                    },\n                    \"html_builder/panel/panel_duo_step\": {\n                        selectLabel: _t(\"Duo Step\"),\n                    },\n                    \"html_builder/panel/panel_duo_step_pill\": {\n                        selectLabel: _t(\"Duo Step Pill\"),\n                    },\n                    \"html_builder/panel/panel_trio_in_r\": {\n                        selectLabel: _t(\"Trio In (R)\"),\n                    },\n                    \"html_builder/panel/panel_trio_out_r\": {\n                        selectLabel: _t(\"Trio Out (R)\"),\n                    },\n                    \"html_builder/panel/panel_window\": {\n                        selectLabel: _t(\"Window\"),\n                        transform: false,\n                        togglableRatio: true,\n                    },\n                },\n            },\n            composites: {\n                label: _t(\"Composites\"),\n                shapes: {\n                    \"html_builder/composite/composite_double_pill\": {\n                        selectLabel: _t(\"Double Pill\"),\n                    },\n                    \"html_builder/composite/composite_triple_pill\": {\n                        selectLabel: _t(\"Triple Pill\"),\n                    },\n                    \"html_builder/composite/composite_half_circle\": {\n                        selectLabel: _t(\"Half Circle\"),\n                    },\n                    \"html_builder/composite/composite_sonar\": {\n                        selectLabel: _t(\"Double Sonar\"),\n                    },\n                    \"html_builder/composite/composite_cut_circle\": {\n                        selectLabel: _t(\"Cut Circle\"),\n                    },\n                },\n            },\n        },\n    },\n    decorative: {\n        label: _t(\"Decorative\"),\n        subgroups: {\n            brushed: {\n                label: _t(\"Brushed\"),\n                shapes: {\n                    \"html_builder/brushed/brush_1\": {\n                        selectLabel: _t(\"Brush 1\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/brushed/brush_2\": {\n                        selectLabel: _t(\"Brush 2\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/brushed/brush_3\": {\n                        selectLabel: _t(\"Brush 3\"),\n                        togglableRatio: true,\n                    },\n                    \"html_builder/brushed/brush_4\": {\n                        selectLabel: _t(\"Brush 4\"),\n                    },\n                },\n            },\n            composition: {\n                label: _t(\"Composition\"),\n                shapes: {\n                    \"html_builder/composition/composition_organic_line\": {\n                        selectLabel: _t(\"Organic Line\"),\n                        transform: false,\n                    },\n                    \"html_builder/composition/composition_oval_line\": {\n                        selectLabel: _t(\"Oval Line\"),\n                        transform: false,\n                    },\n                    \"html_builder/composition/composition_triangle_line\": {\n                        selectLabel: _t(\"Triangle Line\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/composition/composition_line_1\": {\n                        selectLabel: _t(\"Line 1\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/composition/composition_line_3\": {\n                        selectLabel: _t(\"Line 2\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/composition/composition_line_2\": {\n                        selectLabel: _t(\"Line 2\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/composition/composition_mixed_1\": {\n                        selectLabel: _t(\"Mixed 1\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/composition/composition_mixed_2\": {\n                        selectLabel: _t(\"Mixed 2\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/composition/composition_planet_1\": {\n                        selectLabel: _t(\"Planet 1\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/composition/composition_planet_2\": {\n                        selectLabel: _t(\"Planet 2\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/composition/composition_square_1\": {\n                        selectLabel: _t(\"Square Dot 1\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/composition/composition_square_2\": {\n                        selectLabel: _t(\"Square Dot 2\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/composition/composition_square_3\": {\n                        selectLabel: _t(\"Square Dot 3\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/composition/composition_square_4\": {\n                        selectLabel: _t(\"Square Dot 4\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/composition/composition_square_line\": {\n                        selectLabel: _t(\"Square Line\"),\n                        animated: true,\n                        transform: false,\n                    },\n                },\n            },\n            patterns: {\n                label: _t(\"Patterns\"),\n                shapes: {\n                    \"html_builder/pattern/pattern_organic_cross\": {\n                        selectLabel: _t(\"Organic Cross\"),\n                        transform: false,\n                    },\n                    \"html_builder/pattern/pattern_organic_caps\": {\n                        selectLabel: _t(\"Organic Caps\"),\n                        transform: false,\n                    },\n                    \"html_builder/pattern/pattern_oval_zebra\": {\n                        selectLabel: _t(\"Oval Zebra\"),\n                        transform: false,\n                    },\n                    \"html_builder/pattern/pattern_wave_1\": {\n                        selectLabel: _t(\"Wave 1\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/pattern/pattern_line_star\": {\n                        selectLabel: _t(\"Star\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/pattern/pattern_line_sun\": {\n                        selectLabel: _t(\"Sun\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/pattern/pattern_wave_2\": {\n                        selectLabel: _t(\"Wave 2\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/pattern/pattern_wave_3\": {\n                        selectLabel: _t(\"Wave 3\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/pattern/pattern_point\": {\n                        selectLabel: _t(\"Point\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/pattern/pattern_organic_dot\": {\n                        selectLabel: _t(\"Organic Dot\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/pattern/pattern_labyrinth\": {\n                        selectLabel: _t(\"Labyrinth\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/pattern/pattern_circuit\": {\n                        selectLabel: _t(\"Circuit\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/pattern/pattern_wave_4\": {\n                        selectLabel: _t(\"Wave 4\"),\n                        animated: true,\n                        transform: false,\n                    },\n                },\n            },\n            solids: {\n                label: _t(\"Solids\"),\n                shapes: {\n                    \"html_builder/solid/solid_blob_1\": {\n                        selectLabel: _t(\"Blob 1\"),\n                        transform: false,\n                    },\n                    \"html_builder/solid/solid_blob_2\": {\n                        selectLabel: _t(\"Blob 2\"),\n                        transform: false,\n                    },\n                    \"html_builder/solid/solid_blob_3\": {\n                        selectLabel: _t(\"Blob 3\"),\n                        transform: false,\n                    },\n                    \"html_builder/solid/solid_blob_4\": {\n                        selectLabel: _t(\"Blob 4\"),\n                    },\n                    \"html_builder/solid/solid_blob_5\": {\n                        selectLabel: _t(\"Blob 5\"),\n                        transform: false,\n                    },\n                    \"html_builder/solid/solid_blob_shadow_1\": {\n                        selectLabel: _t(\"Blob Shadow 1\"),\n                        transform: false,\n                        animated: true,\n                    },\n                    \"html_builder/solid/solid_blob_shadow_2\": {\n                        selectLabel: _t(\"Blob Shadow 2\"),\n                        transform: false,\n                        animated: true,\n                    },\n                    \"html_builder/solid/solid_square_1\": {\n                        selectLabel: _t(\"Square 1\"),\n                        transform: false,\n                        animated: true,\n                    },\n                    \"html_builder/solid/solid_square_2\": {\n                        selectLabel: _t(\"Square 2\"),\n                        transform: false,\n                        animated: true,\n                    },\n                    \"html_builder/solid/solid_square_3\": {\n                        selectLabel: _t(\"Square 3\"),\n                        transform: false,\n                        animated: true,\n                    },\n                },\n            },\n            specials: {\n                label: _t(\"Specials\"),\n                shapes: {\n                    \"html_builder/special/special_speed\": {\n                        selectLabel: _t(\"Speed\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/special/special_rain\": {\n                        selectLabel: _t(\"Rain\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/special/special_snow\": {\n                        selectLabel: _t(\"Snow\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/special/special_layered\": {\n                        selectLabel: _t(\"Layered\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/special/special_filter\": {\n                        selectLabel: _t(\"Filter\"),\n                        animated: true,\n                        transform: false,\n                    },\n                    \"html_builder/special/special_flag\": {\n                        selectLabel: _t(\"Flag\"),\n                        animated: true,\n                    },\n                    \"html_builder/special/special_organic\": {\n                        selectLabel: _t(\"Organic\"),\n                        animated: true,\n                    },\n                },\n            },\n        },\n    },\n    devices: {\n        label: _t(\"Devices\"),\n        subgroups: {\n            devices: {\n                label: _t(\"Devices\"),\n                shapes: {\n                    \"html_builder/devices/iphone_front_portrait\": {\n                        selectLabel: _t(\"iPhone #1\"),\n                        imgSize: \"0.46:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/iphone_3d_portrait_01\": {\n                        selectLabel: _t(\"iPhone #2\"),\n                        imgSize: \"0.46:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/iphone_3d_portrait_02\": {\n                        selectLabel: _t(\"iPhone #3\"),\n                        imgSize: \"0.46:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/iphone_front_landscape\": {\n                        selectLabel: _t(\"iPhone #4\"),\n                        imgSize: \"2.17:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/iphone_3d_landscape_01\": {\n                        selectLabel: _t(\"iPhone #5\"),\n                        imgSize: \"2.17:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/iphone_3d_landscape_02\": {\n                        selectLabel: _t(\"iPhone #6\"),\n                        imgSize: \"2.17:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/galaxy_front_portrait\": {\n                        selectLabel: _t(\"Galaxy S #1\"),\n                        imgSize: \"0.45:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/galaxy_3d_portrait_01\": {\n                        selectLabel: _t(\"Galaxy S #2\"),\n                        imgSize: \"0.45:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/galaxy_3d_portrait_02\": {\n                        selectLabel: _t(\"Galaxy S #3\"),\n                        imgSize: \"0.45:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/galaxy_front_landscape\": {\n                        selectLabel: _t(\"Galaxy S #4\"),\n                        imgSize: \"2.22:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/galaxy_3d_landscape_01\": {\n                        selectLabel: _t(\"Galaxy S #5\"),\n                        imgSize: \"2.22:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/galaxy_3d_landscape_02\": {\n                        selectLabel: _t(\"Galaxy S #6\"),\n                        imgSize: \"2.22:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/galaxy_front_portrait_half\": {\n                        selectLabel: _t(\"Half Galaxy S\"),\n                        imgSize: \"0.45:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/ipad_front_portrait\": {\n                        selectLabel: _t(\"iPad #1\"),\n                        imgSize: \"0.75:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/ipad_3d_portrait_01\": {\n                        selectLabel: _t(\"iPad #2\"),\n                        imgSize: \"0.75:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/ipad_3d_portrait_02\": {\n                        selectLabel: _t(\"iPad #3\"),\n                        imgSize: \"0.75:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/ipad_front_landscape\": {\n                        selectLabel: _t(\"iPad #4\"),\n                        imgSize: \"4:3\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/ipad_3d_landscape_01\": {\n                        selectLabel: _t(\"iPad #5\"),\n                        imgSize: \"4:3\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/ipad_3d_landscape_02\": {\n                        selectLabel: _t(\"iPad #6\"),\n                        imgSize: \"4:3\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/imac_front\": {\n                        selectLabel: _t(\"iMac #1\"),\n                        imgSize: \"16:9\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/imac_3d_01\": {\n                        selectLabel: _t(\"iMac #2\"),\n                        imgSize: \"16:9\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/imac_3d_02\": {\n                        selectLabel: _t(\"iMac #3\"),\n                        imgSize: \"16:9\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/macbook_front\": {\n                        selectLabel: _t(\"MacBook #1\"),\n                        imgSize: \"1.6:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/macbook_3d_01\": {\n                        selectLabel: _t(\"MacBook #2\"),\n                        imgSize: \"1.6:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/macbook_3d_02\": {\n                        selectLabel: _t(\"MacBook #3\"),\n                        imgSize: \"1.6:1\",\n                        transform: false,\n                    },\n                    \"html_builder/devices/browser_01\": {\n                        selectLabel: _t(\"Browser #1\"),\n                        transform: false,\n                    },\n                    \"html_builder/devices/browser_02\": {\n                        selectLabel: _t(\"Browser #2\"),\n                        transform: false,\n                    },\n                    \"html_builder/devices/browser_03\": {\n                        selectLabel: _t(\"Browser #3\"),\n                        transform: false,\n                    },\n                },\n            },\n        },\n    },\n};\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { getImageSrc } from \"@html_editor/utils/image\";\nimport { getDataURLBinarySize } from \"@html_editor/utils/image_processing\";\n\nconst imageCacheSize = new Map();\n\nexport class ImageSize extends BaseOptionComponent {\n    static template = \"html_builder.ImageSize\";\n\n    setup() {\n        super.setup();\n        this.imagePostProcess = this.env.editor.shared.imagePostProcess;\n        this.state = useDomState(async (el) => ({\n            size: await this.getImageSize(el),\n        }));\n    }\n    async getImageSize(el) {\n        const src = getImageSrc(el);\n        if (!src) {\n            return;\n        }\n        let size;\n        if (isBase64ImageSrc(src)) {\n            size = getDataURLBinarySize(src);\n        } else {\n            if (imageCacheSize.has(src)) {\n                size = imageCacheSize.get(src);\n            } else {\n                size = await this.imagePostProcess.getProcessedImageSize(el);\n                imageCacheSize.set(src, size);\n            }\n        }\n        return `${(size / 1024).toFixed(1)} kB`;\n    }\n}\nfunction isBase64ImageSrc(src) {\n    // Check if it's a data URL with base64 encoding\n    return src && src.startsWith(\"data:image/\") && src.includes(\";base64,\");\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { ImageSize } from \"./image_size\";\n\nclass ImageSizePlugin extends Plugin {\n    static id = \"imageSize\";\n    static dependencies = [\"imagePostProcess\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        elements_to_options_title_components: {\n            Component: ImageSize,\n            selector: \"img\",\n        },\n    };\n}\nregistry.category(\"builder-plugins\").add(ImageSizePlugin.id, ImageSizePlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\n\nclass ImageSnippetOptionPlugin extends Plugin {\n    static id = \"imageSnippetOption\";\n    static dependencies = [\"media\"];\n    static shared = [\"onSnippetDropped\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        on_snippet_dropped_handlers: this.onSnippetDropped.bind(this),\n        so_content_addition_selector: [\".s_image\"],\n    };\n\n    async onSnippetDropped({ snippetEl, dragState }) {\n        if (!snippetEl.matches(\".s_image\")) {\n            return;\n        }\n\n        // Open the media dialog and replace the image snippet placeholder by\n        // the selected image.\n        let isImageSelected = false;\n        await new Promise((resolve) => {\n            const onClose = this.dependencies.media.openMediaDialog({\n                onlyImages: true,\n                save: async (selectedImageEl) => {\n                    isImageSelected = true;\n                    snippetEl.replaceWith(selectedImageEl);\n                    // If the \"Image\" snippet was dropped as a grid item, make\n                    // it a grid image.\n                    if (dragState.draggedEl.classList.contains(\"o_grid_item\")) {\n                        dragState.draggedEl.classList.add(\"o_grid_item_image\");\n                    }\n                },\n            });\n            onClose.then(() => {\n                resolve();\n            });\n        });\n\n        return !isImageSelected;\n    }\n}\n\nregistry.category(\"builder-plugins\").add(ImageSnippetOptionPlugin.id, ImageSnippetOptionPlugin);\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { ImageShapeOption } from \"@html_builder/plugins/image/image_shape_option\";\nimport { ImageFilterOption } from \"@html_builder/plugins/image/image_filter_option\";\nimport { ImageFormatOption } from \"@html_builder/plugins/image/image_format_option\";\nimport { ImageTransformOption } from \"./image_transform_option\";\n\nexport class ImageToolOption extends BaseOptionComponent {\n    static template = \"html_builder.ImageToolOption\";\n    static components = {\n        ImageShapeOption,\n        ImageFilterOption,\n        ImageFormatOption,\n        ImageTransformOption,\n    };\n    static selector = \"img\";\n    static exclude = \"[data-oe-type='image'] > img\";\n    static name = \"imageToolOption\";\n    setup() {\n        super.setup();\n        this.state = useDomState((editingElement) => ({\n            isImageAnimated: editingElement.classList.contains(\"o_animate\"),\n        }));\n    }\n}\n", "import { cropperDataFieldsWithAspectRatio, loadImage } from \"@html_editor/utils/image_processing\";\nimport { registry } from \"@web/core/registry\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { ImageToolOption } from \"./image_tool_option\";\nimport { isImageCorsProtected } from \"@html_editor/utils/image\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport {\n    REPLACE_MEDIA,\n    IMAGE_TOOL,\n    ALIGNMENT_STYLE_PADDING,\n} from \"@html_builder/utils/option_sequence\";\nimport { ReplaceMediaOption, searchSupportedParentLinkEl } from \"./replace_media_option\";\nimport { computeMaxDisplayWidth } from \"@html_builder/plugins/image/image_format_option\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { isCSSColor } from \"@web/core/utils/colors\";\nimport { getCSSVariableValue, getHtmlStyle } from \"@html_editor/utils/formatting\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class ImageAndFaOption extends BaseOptionComponent {\n    static template = \"html_builder.ImageAndFaOption\";\n    static selector = \"span.fa, i.fa, img\";\n    static exclude = \"[data-oe-type='image'] > img, [data-oe-xpath]\";\n    static name = \"imageAndFaOption\";\n}\nclass ImageToolOptionPlugin extends Plugin {\n    static id = \"imageToolOption\";\n    static dependencies = [\n        \"history\",\n        \"userCommand\",\n        \"imagePostProcess\",\n        \"imageCrop\",\n        \"media\",\n        \"builderOptions\",\n    ];\n    static shared = [\"getCSSColorValue\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_options: [\n            withSequence(REPLACE_MEDIA, ReplaceMediaOption),\n            withSequence(IMAGE_TOOL, ImageToolOption),\n            withSequence(ALIGNMENT_STYLE_PADDING, ImageAndFaOption),\n        ],\n        builder_actions: {\n            CropImageAction,\n            ResetCropAction,\n            ReplaceMediaAction,\n            SetLinkAction,\n            SetUrlAction,\n            SetNewWindowAction,\n            AltAction,\n        },\n        on_media_dialog_saved_handlers: async (elements, { node }) => {\n            for (const image of elements) {\n                if (image && image.tagName === \"IMG\") {\n                    const updateImageAttributes =\n                        await this.dependencies.imagePostProcess.processImage({\n                            img: image,\n                            newDataset: {\n                                formatMimetype: this.config.defaultImageMimetype ?? \"image/webp\",\n                            },\n                            // TODO Using a callback is currently needed to avoid\n                            // the extra RPC that would occur if loadImageInfo was\n                            // called before processImage as well. This flow can be\n                            // simplified if image infos are somehow cached.\n                            onImageInfoLoaded: async (dataset) => {\n                                if (!dataset.originalSrc || !dataset.originalId) {\n                                    return true;\n                                }\n                                const original = await loadImage(dataset.originalSrc);\n                                const maxWidth = dataset.width\n                                    ? image.naturalWidth\n                                    : original.naturalWidth;\n                                const optimizedWidth = Math.min(\n                                    maxWidth,\n                                    computeMaxDisplayWidth(node || this.editable)\n                                );\n                                if (\n                                    ![\"image/gif\", \"image/svg+xml\"].includes(\n                                        dataset.mimetypeBeforeConversion\n                                    )\n                                ) {\n                                    // Convert to recommended format and width.\n                                    dataset.resizeWidth = optimizedWidth;\n                                } else if (\n                                    dataset.shape &&\n                                    dataset.mimetypeBeforeConversion !== \"image/gif\"\n                                ) {\n                                    dataset.resizeWidth = optimizedWidth;\n                                } else {\n                                    return true;\n                                }\n                            },\n                        });\n                    updateImageAttributes();\n                }\n            }\n        },\n        hover_effect_allowed_predicates: (el) => this.canHaveHoverEffect(el),\n        normalize_handlers: this.migrateImages.bind(this),\n    };\n    setup() {\n        this.htmlStyle = getHtmlStyle(this.document);\n    }\n    async canHaveHoverEffect(imgEl) {\n        return imgEl.tagName === \"IMG\" && !(await isImageCorsProtected(imgEl));\n    }\n    migrateImages(rootEl) {\n        for (const el of selectElements(\n            rootEl,\n            \"img[data-original-id]:not([data-attachment-id]), .oe_img_bg[data-original-id]:not([data-attachment-id])\"\n        )) {\n            el.dataset.attachmentId = el.dataset.originalId;\n        }\n        for (const el of selectElements(\n            rootEl,\n            \"img[data-original-mimetype]:not([data-format-mimetype]), .oe_img_bg[data-original-mimetype]:not([data-format-mimetype])\"\n        )) {\n            el.dataset.formatMimetype = el.dataset.originalMimetype;\n            delete el.dataset.originalMimetype;\n        }\n    }\n    /**\n     * Gets the CSS value of a color variable name.\n     *\n     * @param {string} color\n     * @returns {string}\n     */\n    getCSSColorValue(color) {\n        if (!color || isCSSColor(color)) {\n            return color;\n        }\n        return getCSSVariableValue(color, this.htmlStyle);\n    }\n}\n\nexport class CropImageAction extends BuilderAction {\n    static id = \"cropImage\";\n    static dependencies = [\"imageCrop\", \"imagePostProcess\"];\n    isApplied({ editingElement }) {\n        return cropperDataFieldsWithAspectRatio.some((field) => editingElement.dataset[field]);\n    }\n    load({ editingElement: img }) {\n        return new Promise((resolve) => {\n            this.dependencies.imageCrop.openCropImage(img, {\n                onClose: resolve,\n                onSave: async (newDataset) => {\n                    resolve(this.dependencies.imagePostProcess.processImage({ img, newDataset }));\n                },\n            });\n        });\n    }\n    apply({ loadResult: updateImageAttributes }) {\n        updateImageAttributes?.();\n    }\n}\n\nexport class ResetCropAction extends BuilderAction {\n    static id = \"resetCrop\";\n    static dependencies = [\"imagePostProcess\"];\n    async load({ editingElement: img }) {\n        const newDataset = Object.fromEntries(\n            cropperDataFieldsWithAspectRatio.map((field) => [field, undefined])\n        );\n        return this.dependencies.imagePostProcess.processImage({ img, newDataset });\n    }\n    apply({ loadResult: updateImageAttributes }) {\n        updateImageAttributes();\n    }\n}\n\nexport class ReplaceMediaAction extends BuilderAction {\n    static id = \"replaceMedia\";\n    static dependencies = [\"media_website\"];\n    async apply({ editingElement: mediaEl }) {\n        await this.dependencies.media_website.replaceMedia(mediaEl);\n    }\n}\nexport class SetLinkAction extends BuilderAction {\n    static id = \"setLink\";\n    setup() {\n        this.preview = false;\n    }\n    apply({ editingElement }) {\n        const parentEl = searchSupportedParentLinkEl(editingElement);\n        if (parentEl.tagName !== \"A\") {\n            const wrapperEl = document.createElement(\"a\");\n            editingElement.after(wrapperEl);\n            wrapperEl.appendChild(editingElement);\n        } else {\n            const fragment = document.createDocumentFragment();\n            fragment.append(...parentEl.childNodes);\n            parentEl.replaceWith(fragment);\n        }\n    }\n    isApplied({ editingElement }) {\n        const parentEl = searchSupportedParentLinkEl(editingElement);\n        return parentEl.tagName === \"A\";\n    }\n}\n\nexport class SetUrlAction extends BuilderAction {\n    static id = \"setUrl\";\n    setup() {\n        this.preview = false;\n    }\n    apply({ editingElement, value }) {\n        const linkEl = searchSupportedParentLinkEl(editingElement);\n        let url = value;\n        if (!url) {\n            // As long as there is no URL, the image is not considered a link.\n            linkEl.removeAttribute(\"href\");\n            return;\n        }\n        if (!url.startsWith(\"/\") && !url.startsWith(\"#\") && !/^([a-zA-Z]*.):.+$/gm.test(url)) {\n            // We permit every protocol (http:, https:, ftp:, mailto:,...).\n            // If none is explicitly specified, we assume it is a http.\n            url = \"http://\" + url;\n        }\n        linkEl.setAttribute(\"href\", url);\n    }\n    getValue({ editingElement }) {\n        const linkEl = searchSupportedParentLinkEl(editingElement);\n        return linkEl.getAttribute(\"href\");\n    }\n}\n\nexport class SetNewWindowAction extends BuilderAction {\n    static id = \"setNewWindow\";\n    setup() {\n        this.preview = false;\n    }\n    apply({ editingElement, value }) {\n        const linkEl = searchSupportedParentLinkEl(editingElement);\n        linkEl.setAttribute(\"target\", \"_blank\");\n    }\n    clean({ editingElement }) {\n        const linkEl = searchSupportedParentLinkEl(editingElement);\n        linkEl.removeAttribute(\"target\");\n    }\n    isApplied({ editingElement }) {\n        const linkEl = searchSupportedParentLinkEl(editingElement);\n        return linkEl.getAttribute(\"target\") === \"_blank\";\n    }\n}\n\nexport class AltAction extends BuilderAction {\n    static id = \"alt\";\n    getValue({ editingElement: imgEl }) {\n        return imgEl.alt;\n    }\n    apply({ editingElement: imgEl, value }) {\n        const trimmedValue = value.trim();\n        if (trimmedValue) {\n            imgEl.alt = trimmedValue;\n            if (imgEl.getAttribute(\"role\") === \"presentation\") {\n                imgEl.removeAttribute(\"role\");\n            }\n        } else {\n            imgEl.removeAttribute(\"alt\");\n        }\n    }\n}\n\nregistry.category(\"builder-plugins\").add(ImageToolOptionPlugin.id, ImageToolOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { useImageTransform } from \"@html_editor/main/media/image_transform_button\";\nimport { onWillDestroy } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\nexport class ImageTransformOption extends BaseOptionComponent {\n    static template = \"website.ImageTransformOption\";\n\n    setup() {\n        super.setup();\n        this.transform = useImageTransform({\n            document: document,\n            closeImageTransformation: this.closeImageTransformation.bind(this),\n            buttonSelector:\n                '[data-action-id=\"transformImage\"], [data-action-id=\"transformImage\"] *',\n        });\n        onWillDestroy(() => {\n            this.closeImageTransformation();\n        });\n    }\n\n    closeImageTransformation() {\n        if (this.transform.isImageTransformationOpen()) {\n            registry.category(\"main_components\").remove(\"ImageTransformation\");\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { ImageTransformation } from \"@html_editor/main/media/image_transformation\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\nexport class ImageTransformOptionPlugin extends Plugin {\n    static id = \"imageTransformOption\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_actions: {\n            TransformImageAction,\n            ResetTransformImageAction,\n        },\n    };\n}\n\nclass TransformImageAction extends BuilderAction {\n    static id = \"transformImage\";\n    static dependencies = [\"history\"];\n    isApplied({ editingElement }) {\n        return editingElement.matches(`[style*=\"transform\"]`);\n    }\n    async apply({\n        editingElement,\n        params: { isImageTransformationOpen, closeImageTransformation },\n    }) {\n        if (!isImageTransformationOpen()) {\n            let changed = false;\n            const deferredTillMounted = new Deferred();\n            registry.category(\"main_components\").add(\"ImageTransformation\", {\n                Component: ImageTransformation,\n                props: {\n                    image: editingElement,\n                    document: this.document,\n                    editable: this.editable,\n                    destroy: () => closeImageTransformation(),\n                    onChange: () => {\n                        changed = true;\n                    },\n                    onApply: () => {\n                        if (changed) {\n                            changed = false;\n                            this.dependencies.history.addStep();\n                        }\n                    },\n                    onComponentMounted: () => {\n                        deferredTillMounted.resolve();\n                    },\n                },\n            });\n            await deferredTillMounted;\n        }\n    }\n}\nclass ResetTransformImageAction extends BuilderAction {\n    static id = \"resetTransformImage\";\n    static dependencies = [\"image\"];\n    apply({ editingElement, params: { mainParam: closeImageTransformation } }) {\n        this.dependencies.image.resetImageTransformation(editingElement);\n        closeImageTransformation();\n    }\n}\n\nregistry.category(\"builder-plugins\").add(ImageTransformOptionPlugin.id, ImageTransformOptionPlugin);\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ReplaceMediaOption extends BaseOptionComponent {\n    static template = \"html_builder.ReplaceMediaOption\";\n    static selector = \"img, .media_iframe_video, span.fa, i.fa\";\n    static exclude =\n        \"[data-oe-xpath], a[href^='/website/social/'] > i.fa, a[class*='s_share_'] > i.fa\";\n    static name = \"replaceMediaOption\";\n    setup() {\n        super.setup();\n        this.state = useDomState((editingElement) => ({\n            tooltipName: this.getTooltipName(editingElement),\n            canSetLink: this.canSetLink(editingElement),\n            hasHref: this.hasHref(editingElement),\n        }));\n    }\n    canSetLink(editingElement) {\n        return (\n            isImageSupportedForStyle(editingElement) &&\n            !searchSupportedParentLinkEl(editingElement).matches(\"a[data-oe-xpath]\") &&\n            !editingElement.classList.contains(\"media_iframe_video\")\n        );\n    }\n    hasHref(editingElement) {\n        const parentEl = searchSupportedParentLinkEl(editingElement);\n        return parentEl.tagName === \"A\" && parentEl.hasAttribute(\"href\");\n    }\n    getTooltipName(editingElement) {\n        const classes = editingElement.classList;\n        if (classes.contains(\"media_iframe_video\")) {\n            return _t(\"Replace Video\");\n        } else if (classes.contains(\"img\")) {\n            return _t(\"Replace Image\");\n        } else if (\n            classes.contains(\"fa\") ||\n            Array.from(classes).some((cls) => cls.startsWith(\"s_share_\"))\n        ) {\n            return _t(\"Replace Icon\");\n        } else {\n            return _t(\"Replace Media\");\n        }\n    }\n}\n\nexport function isImageSupportedForStyle(img) {\n    if (!img.parentElement) {\n        return false;\n    }\n\n    // See also `[data-oe-type='image'] > img` added as data-exclude of some\n    // snippet options.\n    const isTFieldImg = \"oeType\" in img.parentElement.dataset;\n\n    // Editable root elements are technically *potentially* supported here (if\n    // the edited attributes are not computed inside the related view, they\n    // could technically be saved... but as we cannot tell the computed ones\n    // apart from the \"static\" ones, we choose to not support edition at all in\n    // those \"root\" cases).\n    // See also `[data-oe-xpath]` added as data-exclude of some snippet options.\n    const isEditableRootElement = \"oeXpath\" in img.dataset;\n\n    return !isTFieldImg && !isEditableRootElement;\n}\n\nexport function searchSupportedParentLinkEl(editingElement) {\n    const parentEl = editingElement.parentElement;\n    return parentEl.matches(\"figure\") ? parentEl.parentElement : parentEl;\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\n\nexport class ImageFieldPlugin extends Plugin {\n    static id = \"imageField\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        content_editable_selectors: \"[data-oe-field][data-oe-type=image] img\",\n        content_not_editable_selectors: \"[data-oe-field][data-oe-type=image]\",\n    };\n}\n\nregistry.category(\"builder-plugins\").add(ImageFieldPlugin.id, ImageFieldPlugin);\n", "import { SelectNumberColumn } from \"@html_builder/core/select_number_column\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class LayoutColumnOption extends BaseOptionComponent {\n    static template = \"html_builder.LayoutColumnOption\";\n    static components = {\n        SelectNumberColumn,\n    };\n    static selector = \"section.s_features_grid, section.s_process_steps\";\n    static applyTo = \":scope > *:has(> .row), :scope > .s_allow_columns\";\n    static name = \"layoutColumnOption\";\n}\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { getFirstItem, getNbColumns, getRow } from \"@html_builder/utils/column_layout_utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { LayoutColumnOption } from \"@html_builder/plugins/layout_column_option\";\nimport { LAYOUT_COLUMN } from \"@html_builder/utils/option_sequence\";\n\nclass LayoutColumnOptionPlugin extends Plugin {\n    static id = \"LayoutColumnOption\";\n    static dependencies = [\"clone\", \"selection\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_options: [withSequence(LAYOUT_COLUMN, LayoutColumnOption)],\n        on_cloned_handlers: this.onCloned.bind(this),\n        builder_actions: {\n            ChangeColumnCountAction,\n        },\n        selection_placeholder_container_predicates: (container) => {\n            if (container.nodeName === \"DIV\" && container.parentElement.classList.contains(\"row\")) {\n                return true;\n            }\n        },\n        selection_blocker_predicates: (blocker) => {\n            if (blocker.nodeType === Node.ELEMENT_NODE && blocker.classList.contains(\"row\")) {\n                return false;\n            }\n        },\n    };\n    onCloned({ cloneEl }) {\n        const cloneElClassList = cloneEl.classList;\n        const offsetRegex = new RegExp(`^offset-(${this.config.mobileBreakpoint}-)?([0-9]{1,2})$`);\n        const offsetClasses = [...cloneElClassList].filter((cls) => cls.match(offsetRegex));\n        cloneElClassList.remove(...offsetClasses);\n    }\n}\n\nexport class ChangeColumnCountAction extends BuilderAction {\n    static id = \"changeColumnCount\";\n    static dependencies = [\"selection\", \"clone\", \"builderOptions\"];\n    async apply({ editingElement, value: nbColumns }) {\n        if (nbColumns === \"custom\") {\n            return;\n        }\n\n        let rowEl = getRow(editingElement);\n        let columnEls, prevNbColumns;\n        if (!rowEl) {\n            // If there is no row, create one and wrap the content\n            // in a column.\n            const cursors = this.dependencies.selection.preserveSelection();\n            rowEl = document.createElement(\"div\");\n            const columnEl = document.createElement(\"div\");\n            rowEl.classList.add(\"row\");\n            columnEl.classList.add(`col-${this.config.mobileBreakpoint}-12`);\n            columnEl.append(...editingElement.children);\n            rowEl.append(columnEl);\n            editingElement.append(rowEl);\n            cursors.restore();\n\n            columnEls = [columnEl];\n            prevNbColumns = 0;\n        } else {\n            columnEls = rowEl.children;\n            prevNbColumns = getNbColumns(\n                columnEls,\n                this.config.isMobileView(this.editable),\n                this.config.mobileBreakpoint\n            );\n        }\n\n        if (nbColumns === prevNbColumns) {\n            return;\n        }\n        this._resizeColumns(columnEls, nbColumns || 1);\n\n        const itemsDelta = nbColumns - rowEl.children.length;\n        if (itemsDelta > 0) {\n            for (let i = 0; i < itemsDelta; i++) {\n                const lastEl = rowEl.lastElementChild;\n                await this.dependencies.clone.cloneElement(lastEl, { activateClone: false });\n            }\n        }\n\n        // If \"None\" columns was chosen, unwrap the content from\n        // the column and the row and remove them.\n        if (nbColumns === 0) {\n            const cursors = this.dependencies.selection.preserveSelection();\n            const columnEl = editingElement.querySelector(\".row > div\");\n            editingElement.append(...columnEl.children);\n            rowEl.remove();\n            cursors.restore();\n        } else if (prevNbColumns === 0) {\n            // Activate the first column options.\n            const firstColumnEl = editingElement.querySelector(\".row > div\");\n            this.dependencies.builderOptions.setNextTarget(firstColumnEl);\n        }\n    }\n    isApplied({ editingElement, value }) {\n        const columnEls = getRow(editingElement)?.children;\n        return (\n            getNbColumns(\n                columnEls,\n                this.config.isMobileView(this.editable),\n                this.config.mobileBreakpoint\n            ) === value\n        );\n    }\n    /**\n     * Resizes the columns for the mobile or desktop view.\n     *\n     * @private\n     * @param {HTMLCollection} columnEls - the elements to resize\n     * @param {integer} nbColumns - the number of wanted columns\n     */\n    _resizeColumns(columnEls, nbColumns) {\n        const isMobile = this.config.isMobileView(this.editable);\n        const itemSize = Math.floor(12 / nbColumns) || 1;\n        const firstItem = getFirstItem(columnEls, isMobile);\n        const firstItemOffset = Math.floor((12 - itemSize * nbColumns) / 2);\n\n        const resolutionModifier = isMobile ? \"\" : `-${this.config.mobileBreakpoint}`;\n        const replacingRegex =\n            // (?!\\S): following char cannot be a non-space character\n            new RegExp(`(?:^|\\\\s+)(col|offset)${resolutionModifier}(-\\\\d{1,2})?(?!\\\\S)`, \"g\");\n\n        for (const columnEl of columnEls) {\n            columnEl.className = columnEl.className.replace(replacingRegex, \"\");\n            columnEl.classList.add(`col${resolutionModifier}-${itemSize}`);\n            if (firstItemOffset && columnEl === firstItem) {\n                columnEl.classList.add(`offset${resolutionModifier}-${firstItemOffset}`);\n            }\n            const hasMobileOffset = columnEl.className.match(/(^|\\s+)offset-\\d{1,2}(?!\\S)/);\n            const desktopOffsetRegexp = new RegExp(\n                `(^|\\\\s+)offset-${this.config.mobileBreakpoint}-[1-9][0-1]?(?!\\\\S)`\n            );\n            const hasDesktopOffset = columnEl.className.match(desktopOffsetRegexp);\n            columnEl.classList.toggle(\n                `offset-${this.config.mobileBreakpoint}-0`,\n                hasMobileOffset && !hasDesktopOffset\n            );\n        }\n    }\n}\n\nregistry.category(\"builder-plugins\").add(LayoutColumnOptionPlugin.id, LayoutColumnOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { onWillStart } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class Many2OneOption extends BaseOptionComponent {\n    static template = \"html_builder.Many2OneOption\";\n    static selector = \"[data-oe-many2one-model]:not([data-oe-readonly])\";\n    static editableOnly = false;\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        onWillStart(async () => {\n            const el = this.env.getEditingElement();\n            const contactOpts = JSON.parse(el.dataset.oeContactOptions || \"{}\");\n            this.nullText = contactOpts.null_text;\n            this.model = el.dataset.oeMany2oneModel;\n            this.domain = JSON.parse(el.dataset.oeMany2oneDomain || \"[]\");\n            const searchResult = await this.orm.searchRead(\n                \"ir.model\",\n                [[\"model\", \"=\", this.model]],\n                [\"name\"]\n            );\n            this.label = searchResult[0]?.name;\n        });\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { Many2OneOption } from \"./many2one_option\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\n\nexport class Many2OneOptionPlugin extends Plugin {\n    static id = \"many2OneOption\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_options: [Many2OneOption],\n        builder_actions: {\n            Many2OneAction,\n        },\n        content_not_editable_selectors: \"[data-oe-field][data-oe-many2one-id]\",\n        after_replication_handlers: ({ sourceEl, targetEl }) => {\n            if (\n                sourceEl.hasAttribute(\"data-oe-many2one-model\") &&\n                targetEl.hasAttribute(\"data-oe-many2one-model\")\n            ) {\n                targetEl.setAttribute(\n                    \"data-oe-many2one-id\",\n                    sourceEl.getAttribute(\"data-oe-many2one-id\")\n                );\n            }\n        },\n    };\n}\n\nexport class Many2OneAction extends BuilderAction {\n    static id = \"many2One\";\n    async load({ editingElement, value }) {\n        const { id } = JSON.parse(value);\n        const { oeModel, oeId, oeField } = editingElement.dataset;\n        const allContactOptions = new Set(\n            this.editable\n                .querySelectorAll(\n                    `[data-oe-model=\"${oeModel}\"][data-oe-id=\"${oeId}\"][data-oe-field=\"${oeField}\"][data-oe-type=\"contact\"]`\n                )\n                .values()\n                .map((el) => el.dataset.oeContactOptions)\n        );\n        return Object.fromEntries(\n            await Promise.all(\n                allContactOptions\n                    .values()\n                    .map(async (contactOptions) => [\n                        contactOptions,\n                        await this.services.orm.call(\n                            \"ir.qweb.field.contact\",\n                            \"get_record_to_html\",\n                            [[id]],\n                            { options: JSON.parse(contactOptions) }\n                        ),\n                    ])\n            )\n        );\n    }\n    apply({ editingElement, value, loadResult }) {\n        const { id, name } = JSON.parse(value);\n        const { oeModel, oeId, oeField, oeContactOptions } = editingElement.dataset;\n\n        const selector =\n            `[data-oe-model=\"${oeModel}\"][data-oe-id=\"${oeId}\"][data-oe-field=\"${oeField}\"]` +\n            (oeContactOptions === undefined\n                ? \"[data-oe-contact-options]\"\n                : `:not([data-oe-contact-options='${oeContactOptions}'])`);\n        for (const el of [...this.editable.querySelectorAll(selector), editingElement]) {\n            el.dataset.oeMany2oneId = id;\n            if (el.dataset.oeType === \"contact\") {\n                el.replaceChildren(\n                    ...new DOMParser().parseFromString(\n                        loadResult[el.dataset.oeContactOptions],\n                        \"text/html\"\n                    ).body.childNodes\n                );\n            } else {\n                el.textContent = name;\n            }\n        }\n    }\n    getValue({ editingElement }) {\n        return JSON.stringify({ id: parseInt(editingElement.dataset.oeMany2oneId) });\n    }\n}\n\nregistry.category(\"builder-plugins\").add(Many2OneOptionPlugin.id, Many2OneOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\n\nconst monetarySel = \"[data-oe-field][data-oe-type=monetary]\";\n\nexport class MonetaryFieldPlugin extends Plugin {\n    static id = \"monetaryField\";\n    static dependencies = [\"selection\"];\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        content_editable_selectors: `${monetarySel} .oe_currency_value`,\n        content_not_editable_selectors: monetarySel,\n    };\n\n    setup() {\n        const handleEvent = (ev, condition) => {\n            const fieldEl = ev.target.closest(monetarySel);\n            const amountEl = fieldEl?.querySelector(\".oe_currency_value\");\n            if (amountEl?.isContentEditable && condition({ fieldEl, amountEl })) {\n                this.dependencies.selection.setSelection({\n                    anchorNode: amountEl,\n                    anchorOffset: 0,\n                    focusNode: amountEl,\n                    focusOffset: amountEl.childNodes.length,\n                });\n            }\n        };\n        this.addDomListener(this.editable, \"focusin\", (ev) =>\n            handleEvent(ev, ({ fieldEl }) => fieldEl !== ev.relatedTarget?.closest(monetarySel))\n        );\n        this.addDomListener(this.editable, \"click\", (ev) =>\n            handleEvent(\n                ev,\n                ({ amountEl }) =>\n                    !this.dependencies.selection\n                        .getTargetedNodes()\n                        .some((node) => amountEl.contains(node))\n            )\n        );\n    }\n}\n\nregistry.category(\"builder-plugins\").add(MonetaryFieldPlugin.id, MonetaryFieldPlugin);\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\n\nexport class RatingOption extends BaseOptionComponent {\n    static template = \"html_builder.RatingOption\";\n    static selector = \".s_rating\";\n}\n\nclass RatingOptionPlugin extends Plugin {\n    static id = \"ratingOption\";\n    static dependencies = [\"history\", \"media\"];\n    selector = \".s_rating\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_options: RatingOption,\n        so_content_addition_selector: [\".s_rating\"],\n        builder_actions: {\n            SetIconsAction,\n            CustomIconAction,\n            ActiveIconsNumberAction,\n            TotalIconsNumberAction,\n        },\n    };\n}\n\nexport class SetIconsAction extends BuilderAction {\n    static id = \"setIcons\";\n    apply({ editingElement, params: { mainParam: iconParam } }) {\n        editingElement.dataset.icon = iconParam;\n        renderIcons(editingElement);\n        delete editingElement.dataset.activeCustomIcon;\n        delete editingElement.dataset.inactiveCustomIcon;\n    }\n    isApplied({ editingElement, params: { mainParam: iconParam } }) {\n        return getIconType(editingElement) === iconParam;\n    }\n}\nexport class CustomIconAction extends BuilderAction {\n    static id = \"customIcon\";\n    static dependencies = [\"media\"];\n    async load({ editingElement, params: { mainParam: customParam } }) {\n        return new Promise((resolve) => {\n            const isCustomActive = customParam === \"customActiveIcon\";\n            const media = document.createElement(\"i\");\n            media.className = isCustomActive\n                ? getActiveCustomIcons(editingElement)\n                : getInactiveCustomIcons(editingElement);\n            const mediaDialogParams = {\n                visibleTabs: [\"ICONS\"],\n                media,\n                save: (icon) => {\n                    resolve(icon);\n                },\n            };\n            const onClose = this.dependencies.media.openMediaDialog(\n                mediaDialogParams,\n                this.editable\n            );\n            onClose.then(resolve);\n        });\n    }\n    apply({ editingElement, loadResult: savedIconEl, params: { mainParam: customParam } }) {\n        if (!savedIconEl) {\n            return;\n        }\n        const isCustomActive = customParam === \"customActiveIcon\";\n        const customClass = savedIconEl.className;\n        const activeIconEls = getActiveIcons(editingElement);\n        const inactiveIconEls = getInactiveIcons(editingElement);\n        const iconEls = isCustomActive ? activeIconEls : inactiveIconEls;\n        iconEls.forEach((iconEl) => (iconEl.className = customClass));\n        const faClassActiveCustomIcons =\n            activeIconEls.length > 0 ? activeIconEls[0].getAttribute(\"class\") : customClass;\n        const faClassInactiveCustomIcons =\n            inactiveIconEls.length > 0 ? inactiveIconEls[0].getAttribute(\"class\") : customClass;\n        editingElement.dataset.activeCustomIcon = faClassActiveCustomIcons;\n        editingElement.dataset.inactiveCustomIcon = faClassInactiveCustomIcons;\n        editingElement.dataset.icon = \"custom\";\n    }\n}\nexport class ActiveIconsNumberAction extends BuilderAction {\n    static id = \"activeIconsNumber\";\n    apply({ editingElement, value }) {\n        const nbActiveIcons = parseInt(value);\n        const nbTotalIcons = getAllIcons(editingElement).length;\n        createIcons({\n            editingElement: editingElement,\n            nbActiveIcons: nbActiveIcons,\n            nbTotalIcons: nbTotalIcons,\n        });\n    }\n    getValue({ editingElement }) {\n        return getActiveIcons(editingElement).length;\n    }\n}\nexport class TotalIconsNumberAction extends BuilderAction {\n    static id = \"totalIconsNumber\";\n    apply({ editingElement, value }) {\n        const nbTotalIcons = Math.max(parseInt(value), 1);\n        const nbActiveIcons = getActiveIcons(editingElement).length;\n        createIcons({\n            editingElement: editingElement,\n            nbActiveIcons: nbActiveIcons,\n            nbTotalIcons: nbTotalIcons,\n        });\n    }\n    getValue({ editingElement }) {\n        return getAllIcons(editingElement).length;\n    }\n}\n\nregistry.category(\"builder-plugins\").add(RatingOptionPlugin.id, RatingOptionPlugin);\n\nfunction createIcons({ editingElement, nbActiveIcons, nbTotalIcons }) {\n    const activeIconEl = editingElement.querySelector(\".s_rating_active_icons\");\n    const inactiveIconEl = editingElement.querySelector(\".s_rating_inactive_icons\");\n    const iconEls = getAllIcons(editingElement);\n    [...iconEls].forEach((iconEl) => iconEl.remove());\n    for (let i = 0; i < nbTotalIcons; i++) {\n        const targetEl = i < nbActiveIcons ? activeIconEl : inactiveIconEl;\n        targetEl.appendChild(document.createElement(\"i\"));\n        targetEl.appendChild(document.createTextNode(\" \"));\n    }\n    renderIcons(editingElement);\n}\nfunction getActiveCustomIcons(editingElement) {\n    return editingElement.dataset.activeCustomIcon || \"\";\n}\nfunction getActiveIcons(editingElement) {\n    return editingElement.querySelectorAll(\".s_rating_active_icons > i\");\n}\nfunction getAllIcons(editingElement) {\n    return editingElement.querySelectorAll(\".s_rating_icons i\");\n}\nfunction getIconType(editingElement) {\n    return editingElement.dataset.icon;\n}\nfunction getInactiveCustomIcons(editingElement) {\n    return editingElement.dataset.inactiveCustomIcon || \"\";\n}\nfunction getInactiveIcons(editingElement) {\n    return editingElement.querySelectorAll(\".s_rating_inactive_icons  > i\");\n}\nfunction renderIcons(editingElement) {\n    const iconType = getIconType(editingElement);\n    const icons = {\n        \"fa-star\": \"fa-star-o\",\n        \"fa-thumbs-up\": \"fa-thumbs-o-up\",\n        \"fa-circle\": \"fa-circle-o\",\n        \"fa-square\": \"fa-square-o\",\n        \"fa-heart\": \"fa-heart-o\",\n    };\n    const faClassActiveIcons =\n        iconType === \"custom\" ? getActiveCustomIcons(editingElement) : \"fa \" + iconType;\n    const faClassInactiveIcons =\n        iconType === \"custom\" ? getInactiveCustomIcons(editingElement) : \"fa \" + icons[iconType];\n    const activeIconEls = getActiveIcons(editingElement);\n    const inactiveIconEls = getInactiveIcons(editingElement);\n    activeIconEls.forEach((activeIconEl) => (activeIconEl.className = faClassActiveIcons));\n    inactiveIconEls.forEach((inactiveIconEl) => (inactiveIconEl.className = faClassInactiveIcons));\n}\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { BorderConfigurator } from \"./border_configurator_option\";\n\nclass SeparatorOptionPlugin extends Plugin {\n    static id = \"separatorOption\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_options: [SeparatorOption],\n        dropzone_selector: {\n            selector: \".s_hr\",\n            dropNear: \"p, h1, h2, h3, blockquote, .s_hr\",\n        },\n        so_content_addition_selector: [\".s_hr\"],\n        is_movable_selector: { selector: \".s_hr\", direction: \"vertical\" },\n    };\n}\n\nexport class SeparatorOption extends BaseOptionComponent {\n    static template = \"html_builder.SeparatorOption\";\n    static selector = \".s_hr\";\n    static applyTo = \"hr\";\n    static components = { BorderConfigurator };\n}\nregistry.category(\"builder-plugins\").add(SeparatorOptionPlugin.id, SeparatorOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class ShadowOption extends BaseOptionComponent {\n    static template = \"html_builder.ShadowOption\";\n    static props = {\n        setShadowModeAction: { type: String, optional: true },\n        setShadowAction: { type: String, optional: true },\n    };\n    static defaultProps = {\n        setShadowModeAction: \"setShadowMode\",\n        setShadowAction: \"setShadow\",\n    };\n}\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { parseBoxShadow } from \"@html_builder/utils/utils_css\";\n\nconst shadowClass = \"shadow\";\n\nexport class ShadowOptionPlugin extends Plugin {\n    static id = \"shadowOption\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_actions: {\n            SetShadowModeAction,\n            SetShadowAction,\n        },\n    };\n}\n\nexport function getDefaultShadow(mode) {\n    const el = document.createElement(\"div\");\n    el.classList.add(shadowClass);\n    document.body.appendChild(el);\n    const shadow = `${getComputedStyle(el).boxShadow}${mode === \"inset\" ? \" inset\" : \"\"}`;\n    el.remove();\n    return shadow;\n}\n\nfunction getShadowMode(editingElement) {\n    const currentBoxShadow = getComputedStyle(editingElement)[\"box-shadow\"];\n    if (currentBoxShadow === \"none\") {\n        return \"none\";\n    }\n    if (currentBoxShadow.includes(\"inset\")) {\n        return \"inset\";\n    }\n    if (!currentBoxShadow.includes(\"inset\") && currentBoxShadow !== \"none\") {\n        return \"outset\";\n    }\n}\n\nfunction setBoxShadow(editingElement, value) {\n    editingElement.style.setProperty(\"box-shadow\", value, \"important\");\n}\n\nexport function getCurrentShadow(editingElement) {\n    return parseShadow(getComputedStyle(editingElement)[\"box-shadow\"]);\n}\n\nfunction parseShadow(value) {\n    if (!value || value === \"none\") {\n        return {};\n    }\n    return parseBoxShadow(value);\n}\n\nexport function shadowToString(shadow) {\n    if (!shadow) {\n        return \"\";\n    }\n    return `${shadow.color} ${shadow.offsetX} ${shadow.offsetY} ${shadow.blur} ${shadow.spread} ${\n        shadow.mode ? shadow.mode : \"\"\n    }`;\n}\n\nregistry.category(\"builder-plugins\").add(ShadowOptionPlugin.id, ShadowOptionPlugin);\n\nexport class SetShadowModeAction extends BuilderAction {\n    static id = \"setShadowMode\";\n    isApplied({ editingElement, value: shadowMode }) {\n        return shadowMode === getShadowMode(editingElement);\n    }\n    getValue({ editingElement }) {\n        return getShadowMode(editingElement, \"mode\");\n    }\n    apply({ editingElement, value: shadowMode }) {\n        if (shadowMode === \"none\") {\n            editingElement.classList.remove(shadowClass);\n            setBoxShadow(editingElement, \"\");\n            return;\n        }\n\n        if (!editingElement.classList.contains(shadowClass)) {\n            editingElement.classList.add(shadowClass);\n        }\n        if (editingElement.style[\"box-shadow\"] === \"\") {\n            setBoxShadow(editingElement, getDefaultShadow(shadowMode));\n        } else {\n            const shadow = getCurrentShadow(editingElement);\n            if (shadowMode === \"inset\") {\n                shadow.mode = \"inset\";\n            } else {\n                shadow.mode = \"\";\n            }\n            setBoxShadow(editingElement, shadowToString(shadow));\n        }\n    }\n}\nexport class SetShadowAction extends BuilderAction {\n    static id = \"setShadow\";\n    apply({ editingElement, params: { mainParam: attributeName }, value }) {\n        const shadow = getCurrentShadow(editingElement);\n        shadow[attributeName] = value;\n        setBoxShadow(editingElement, shadowToString(shadow));\n    }\n    getValue({ editingElement, params: { mainParam: attributeName } }) {\n        return getCurrentShadow(editingElement)[attributeName];\n    }\n}\n", "import { useRef, useState } from \"@odoo/owl\";\nimport { ImgGroup } from \"@html_builder/core/img_group\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { useThrottleForAnimation } from \"@web/core/utils/timing\";\nimport { getShapeURL } from \"../image/image_helpers\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { useAutofocus } from \"@web/core/utils/hooks\";\n\nexport class ShapeSelector extends BaseOptionComponent {\n    static template = \"html_builder.shapeSelector\";\n    static props = {\n        onClose: Function,\n        selectorTitle: String,\n        shapeGroups: Object,\n        shapeActionId: String,\n        buttonWrapperClassName: { type: String, optional: true },\n        imgThroughDiv: { type: Boolean, optional: true },\n        getShapeUrl: { type: Function, optional: true },\n    };\n    static components = { ImgGroup };\n\n    setup() {\n        super.setup();\n        this.rootRef = useRef(\"root\");\n        this.tabsRef = useRef(\"tabs\");\n        this.state = useState({ activeGroup: \"basic\" });\n        this.onScroll = useThrottleForAnimation(this._onScroll);\n        useHotkey(\"escape\", () => this.props.onClose());\n        useAutofocus({ refName: \"backButton\" });\n    }\n    getShapeUrl(shapePath) {\n        return this.props.getShapeUrl ? this.props.getShapeUrl(shapePath) : getShapeURL(shapePath);\n    }\n    getShapeClass(shapePath) {\n        return `o_${shapePath.replaceAll(\"/\", \"_\")}`;\n    }\n    scrollToShapes(id) {\n        const container = this.rootRef.el;\n        const selectedElement = container?.querySelector(`[data-shape-group-id=\"${id}\"]`);\n        if (container && selectedElement) {\n            container.scrollTop = selectedElement.offsetTop - container.offsetTop;\n        }\n    }\n\n    _onScroll() {\n        const pagerContainerRect = this.rootRef.el.getBoundingClientRect();\n        // The threshold for when a menu element is defined as 'active' is half\n        // of the container's height. This has a drawback as if a section\n        // is too small it might never get `active` if it's the last section.\n        const threshold = pagerContainerRect.height / 2;\n\n        const anchorEls = this.tabsRef.el.querySelectorAll(\".o-hb-select-pager-tab\");\n        for (const anchorEl of anchorEls) {\n            const groupId = anchorEl.dataset.groupId;\n            const sectionEl = this.rootRef.el.querySelector(`[data-shape-group-id=\"${groupId}\"]`);\n            const nextSectionEl = sectionEl.nextElementSibling;\n\n            const sectionTop = sectionEl.getBoundingClientRect().top - pagerContainerRect.top;\n            const nextSectionTop =\n                nextSectionEl && nextSectionEl.getBoundingClientRect().top - pagerContainerRect.top;\n            if (sectionTop < threshold && (!nextSectionEl || nextSectionTop > threshold)) {\n                this.state.activeGroup = groupId;\n            }\n        }\n    }\n}\n", "import { TEXT_ALIGNMENT } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nclass TextAlignmentOptionPlugin extends Plugin {\n    static id = \"textAlignmentOption\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_options: [withSequence(TEXT_ALIGNMENT, TextAlignmentOption)],\n    };\n}\n\nexport class TextAlignmentOption extends BaseOptionComponent {\n    static template = \"html_builder.TextAlignmentOption\";\n    static selector = \".s_share, .s_text_highlight, .s_social_media\";\n}\n\nregistry.category(\"builder-plugins\").add(TextAlignmentOptionPlugin.id, TextAlignmentOptionPlugin);\n", "export function applyFunDependOnSelectorAndExclude(fn, rootEl, selectorParams) {\n    const editingEls = getEditingEls(rootEl, selectorParams);\n    if (!editingEls.length) {\n        return false;\n    }\n    return Promise.all(editingEls.map((el) => fn(el)));\n}\n\nexport function getEditingEls(rootEl, { selector, exclude, applyTo }) {\n    const closestSelector = rootEl.closest(selector);\n    let editingEls = closestSelector ? [closestSelector] : [...rootEl.querySelectorAll(selector)];\n    if (exclude) {\n        editingEls = editingEls.filter((selectorEl) => !selectorEl.matches(exclude));\n    }\n    if (!applyTo) {\n        return editingEls;\n    }\n    const targetEls = [];\n    for (const editingEl of editingEls) {\n        const applyToEls = applyTo ? editingEl.querySelectorAll(applyTo) : [editingEl];\n        targetEls.push(...applyToEls);\n    }\n    return targetEls;\n}\n", "import { BaseVerticalAlignmentOption } from \"./base_vertical_alignment_option\";\n\nexport class VerticalAlignmentOption extends BaseVerticalAlignmentOption {\n    static selector =\n        \".s_text_image, .s_image_text, .s_three_columns, .s_showcase, .s_numbers, .s_faq_collapse, .s_references, .s_accordion_image, .s_shape_image, .s_reviews_wall\";\n    static applyTo = \".row\";\n    static name = \"verticalAlignmentOption\";\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { ClassAction } from \"@html_builder/core/core_builder_action_plugin\";\nimport { VerticalAlignmentOption } from \"@html_builder/plugins/vertical_alignment_option\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { VERTICAL_ALIGNMENT } from \"@html_builder/utils/option_sequence\";\n\nexport class VerticalAlignmentOptionPlugin extends Plugin {\n    static id = \"verticalAlignmentOption\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_options: [withSequence(VERTICAL_ALIGNMENT, VerticalAlignmentOption)],\n        builder_actions: {\n            SetVerticalAlignmentAction,\n        },\n    };\n}\n\nexport class SetVerticalAlignmentAction extends ClassAction {\n    static id = \"setVerticalAlignment\";\n    getPriority({ params: { mainParam: classNames } = { mainParam: \"\" } }) {\n        return classNames === \"align-items-stretch\" ? 0 : 1;\n    }\n    isApplied({ params: { mainParam: classNames } }) {\n        if (classNames === \"align-items-stretch\") {\n            return true;\n        }\n        return super.isApplied(...arguments);\n    }\n}\n\nregistry\n    .category(\"builder-plugins\")\n    .add(VerticalAlignmentOptionPlugin.id, VerticalAlignmentOptionPlugin);\n", "import { END } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nclass VerticalJustifyOptionPlugin extends Plugin {\n    static id = \"verticalJustifyOption\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_options: [withSequence(END, VerticalJustifyOption)],\n    };\n}\n\nexport class VerticalJustifyOption extends BaseOptionComponent {\n    static template = \"html_builder.VerticalJustifyOption\";\n    static selector =\n        \".s_masonry_block .o_grid_item, .s_quadrant .o_grid_item, .s_banner_categories .o_grid_item\";\n    static exclude = \".o_grid_item_image\";\n}\n\nregistry\n    .category(\"builder-plugins\")\n    .add(VerticalJustifyOptionPlugin.id, VerticalJustifyOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\nimport { WIDTH } from \"@html_builder/utils/option_sequence\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nclass WidthOptionPlugin extends Plugin {\n    static id = \"widthOption\";\n    /** @type {import(\"plugins\").BuilderResources} */\n    resources = {\n        builder_options: [withSequence(WIDTH, WidthOption)],\n    };\n}\n\nexport class WidthOption extends BaseOptionComponent {\n    static template = \"html_builder.WidthOption\";\n    static selector = \".s_alert, .s_blockquote, .s_text_highlight\";\n    static name = \"widthOption\";\n}\nregistry.category(\"builder-plugins\").add(WidthOptionPlugin.id, WidthOptionPlugin);\n", "import { Component, onMounted, onWillDestroy, useRef, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Tooltip } from \"@web/core/tooltip/tooltip\";\nimport { closestScrollableY, getScrollingElement, isScrollableY } from \"@web/core/utils/scrolling\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { closest } from \"@web/core/utils/ui\";\nimport { useDragAndDrop } from \"@html_editor/utils/drag_and_drop\";\nimport { getCSSVariableValue } from \"@html_editor/utils/formatting\";\nimport { useSnippets } from \"@html_builder/snippets/snippet_service\";\nimport { scrollTo } from \"@html_builder/utils/scrolling\";\nimport { Snippet } from \"./snippet\";\nimport { CustomInnerSnippet } from \"./custom_inner_snippet\";\n\n/**\n * @typedef {import(\"@html_builder/core/drag_and_drop_plugin\").DragState} DragState\n * @typedef {((arg: { snippetEl: HTMLElement }) => void)[]} on_snippet_dropped_handlers\n * @typedef {((arg: { snippetEl: HTMLElement, dragState: DragState }) => void)[]} on_snippet_dragged_handlers\n * @typedef {((arg: { droppedEl: HTMLElement, dropzoneEl: HTMLElement, dragState: DragState }) => void)[]} on_snippet_dropped_near_handlers\n * @typedef {((arg: { droppedEl: HTMLElement, dragState: DragState }) => void)[]} on_snippet_dropped_over_handlers\n * @typedef {((arg: { droppedEl: HTMLElement, dragState: DragState, x, y }) => void)[]} on_snippet_move_handlers\n * @typedef {((arg: { droppedEl: HTMLElement, dragState: DragState }) => void)[]} on_snippet_out_dropzone_handlers\n * @typedef {((arg: { droppedEl: HTMLElement, dragState: DragState }) => void)[]} on_snippet_over_dropzone_handlers\n */\n\nexport class BlockTab extends Component {\n    static template = \"html_builder.BlockTab\";\n    static components = { Snippet, CustomInnerSnippet };\n    static props = {\n        snippetsName: String,\n    };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.popover = useService(\"popover\");\n        this.snippetModel = useSnippets(this.props.snippetsName);\n        this.blockTabRef = useRef(\"block-tab\");\n        // Needed to avoid race condition in tours.\n        this.state = useState({ ongoingInsertion: false });\n\n        onMounted(() => {\n            this.makeSnippetDraggable();\n        });\n\n        onWillDestroy(() => {\n            this.draggableComponent?.destroy();\n        });\n    }\n\n    get document() {\n        return this.env.editor.document;\n    }\n\n    get editable() {\n        return this.env.editor.editable;\n    }\n\n    get shared() {\n        return this.env.editor.shared;\n    }\n\n    /**\n     * Opens and manages the snippet dialog after clicking on a snippet group,\n     * and inserts the selected snippet in the page.\n     *\n     * @param {Object} snippet the clicked snippet group\n     */\n    onSnippetGroupClick(snippet) {\n        this.shared.operation.next(\n            async () => {\n                this.cancelDragAndDrop = this.shared.history.makeSavePoint();\n                this.dragState = {};\n                let snippetEl;\n                const baseSectionEl = snippet.content.cloneNode(true);\n                this.state.ongoingInsertion = true;\n                await new Promise((resolve) => {\n                    this.snippetModel.openSnippetDialog(\n                        snippet,\n                        {\n                            onSelect: (snippet) => {\n                                snippetEl = snippet.content.cloneNode(true);\n\n                                // Add the dropzones corresponding to a section and\n                                // make them invisible.\n                                const selectors = this.shared.dropzone.getSelectors(baseSectionEl);\n                                const dropzoneEls =\n                                    this.shared.dropzone.activateDropzones(selectors);\n                                this.editable\n                                    .querySelectorAll(\".oe_drop_zone\")\n                                    .forEach((dropzoneEl) => dropzoneEl.classList.add(\"invisible\"));\n\n                                // Find the dropzone closest to the center of the\n                                // viewport and not located in the top quarter of\n                                // the viewport.\n                                const iframeWindow = this.document.defaultView;\n                                const viewPortCenterPoint = {\n                                    x: iframeWindow.innerWidth / 2,\n                                    y: iframeWindow.innerHeight / 2,\n                                };\n                                const validDropzoneEls = dropzoneEls.filter(\n                                    (el) =>\n                                        el.getBoundingClientRect().top >= viewPortCenterPoint.y / 2\n                                );\n                                const closestDropzoneEl =\n                                    closest(validDropzoneEls, viewPortCenterPoint) ||\n                                    dropzoneEls.at(-1);\n\n                                // Insert the selected snippet.\n                                closestDropzoneEl.after(snippetEl);\n                                this.shared.dropzone.removeDropzones();\n                                return snippetEl;\n                            },\n                            onClose: () => {\n                                resolve();\n                            },\n                        },\n                        this.env.editor\n                    );\n                });\n\n                if (snippetEl) {\n                    await scrollTo(snippetEl, { extraOffset: 50 });\n                    await this.processDroppedSnippet(snippetEl);\n                }\n                this.state.ongoingInsertion = false;\n                delete this.cancelDragAndDrop;\n            },\n            {\n                withLoadingEffect: false,\n                shouldInterceptClick: true,\n            }\n        );\n    }\n\n    /**\n     * Opens and manages the snippet dialog after dropping a snippet group.\n     * If a snippet is selected in the dialog, it will replace the given\n     * placeholder snippet.\n     *\n     * @param {Object} snippet the dropped snippet group\n     * @param {HTMLElement} hookEl the placeholder snippet\n     */\n    async onSnippetGroupDrop(snippet, hookEl) {\n        this.state.ongoingInsertion = true;\n        // Exclude the snippets that are not allowed to be dropped at the\n        // current position.\n        const hookParentEl = hookEl.parentElement;\n        this.snippetModel.snippetStructures.forEach((snippet) => {\n            const { selectorChildren } = this.shared.dropzone.getSelectors(snippet.content);\n            snippet.isExcluded = ![...selectorChildren].some((el) => el === hookParentEl);\n        });\n\n        // Open the snippet dialog.\n        let selectedSnippetEl;\n        await new Promise((resolve) => {\n            this.snippetModel.openSnippetDialog(\n                snippet,\n                {\n                    onSelect: (snippet) => {\n                        selectedSnippetEl = snippet.content.cloneNode(true);\n                        hookEl.replaceWith(selectedSnippetEl);\n                        return selectedSnippetEl;\n                    },\n                    onClose: () => {\n                        if (!selectedSnippetEl) {\n                            hookEl.remove();\n                        }\n                        this.snippetModel.snippetStructures.forEach(\n                            (snippet) => delete snippet.isExcluded\n                        );\n                        resolve();\n                    },\n                },\n                this.env.editor\n            );\n        });\n\n        if (selectedSnippetEl) {\n            await scrollTo(selectedSnippetEl, { extraOffset: 50 });\n            await this.processDroppedSnippet(selectedSnippetEl);\n        } else {\n            this.cancelDragAndDrop();\n        }\n        this.state.ongoingInsertion = false;\n        delete this.cancelDragAndDrop;\n    }\n\n    /**\n     * Shows a tooltip telling to drag the snippet when clicking on it.\n     *\n     * @param {Event} ev\n     */\n    showSnippetTooltip(ev) {\n        const snippetEl = ev.currentTarget.closest(\".o_snippet.o_draggable\");\n        if (snippetEl) {\n            this.hideSnippetToolTip?.();\n            this.hideSnippetToolTip = this.popover.add(snippetEl, Tooltip, {\n                tooltip: _t(\"Drag and drop the building block\"),\n            });\n            setTimeout(this.hideSnippetToolTip, 1500);\n        }\n    }\n\n    // TODO bounce animation on click if empty editable\n\n    /**\n     * Initializes the drag and drop for the snippets in the block tabs.\n     */\n    makeSnippetDraggable() {\n        let dropzoneEls = [];\n        let dragAndDropResolve;\n\n        let snippet, snippetEl, isSnippetGroup;\n\n        const iframeWindow =\n            this.document.defaultView !== window ? this.document.defaultView : false;\n\n        const scrollingElement = () => {\n            let scrollingElement =\n                this.shared.dropzone.getDropRootElement() ||\n                getScrollingElement(this.document) ||\n                this.editable.querySelector(\".o_editable\");\n            if (!isScrollableY(scrollingElement)) {\n                scrollingElement =\n                    closestScrollableY(this.document.defaultView.frameElement) ?? scrollingElement;\n            }\n            return scrollingElement;\n        };\n\n        const dragAndDropOptions = {\n            ref: { el: this.blockTabRef.el },\n            iframeWindow,\n            cursor: \"move\",\n            el: this.blockTabRef.el,\n            elements: \".o_snippet.o_draggable\",\n            scrollingElement,\n            handle: \".o_snippet_thumbnail:not(.o_we_ongoing_insertion .o_snippet_thumbnail)\",\n            dropzones: () => dropzoneEls,\n            helper: ({ element, helperOffset }) => {\n                snippet = element;\n                const draggedEl = element.cloneNode(true);\n                draggedEl\n                    .querySelectorAll(\n                        \".o_snippet_thumbnail_title, .o_snippet_thumbnail_area, .rename-delete-buttons\"\n                    )\n                    .forEach((el) => el.remove());\n                draggedEl.style.position = \"fixed\";\n                document.body.append(draggedEl);\n                // Center the helper on the thumbnail image.\n                const thumbnailImgEl = element.querySelector(\".o_snippet_thumbnail_img\");\n                helperOffset.x = thumbnailImgEl.offsetWidth / 2;\n                helperOffset.y = thumbnailImgEl.offsetHeight / 2;\n                return draggedEl;\n            },\n            onDragStart: ({ element }) => {\n                const dragAndDropProm = new Promise(\n                    (resolve) => (dragAndDropResolve = () => resolve())\n                );\n                this.shared.operation.next(async () => await dragAndDropProm, {\n                    withLoadingEffect: false,\n                });\n                const restoreDragSavePoint = this.shared.history.makeSavePoint();\n                this.cancelDragAndDrop = () => {\n                    this.shared.dropzone.removeDropzones();\n                    // Undo the changes needed to ease the drag and drop.\n                    this.dragState.restoreCallbacks?.forEach((restore) => restore());\n                    restoreDragSavePoint();\n                };\n                this.hideSnippetToolTip?.();\n\n                this.document.body.classList.add(\"oe_dropzone_active\");\n                this.state.ongoingInsertion = true;\n\n                this.dragState = {};\n                dropzoneEls = [];\n\n                // Stop marking the elements with mutations as dirty and make\n                // some changes on the page to ease the drag and drop.\n                const restoreCallbacks = [];\n                for (const prepareDrag of this.env.editor.getResource(\"on_prepare_drag_handlers\")) {\n                    const restore = prepareDrag();\n                    restoreCallbacks.unshift(restore);\n                }\n                this.dragState.restoreCallbacks = restoreCallbacks;\n\n                const category = element.closest(\".o_snippets_container\").id;\n                const id = element.dataset.id;\n                snippet = this.snippetModel.getSnippet(category, id);\n                snippetEl = snippet.content.cloneNode(true);\n                isSnippetGroup = category === \"snippet_groups\";\n\n                // Check if the snippet is inline. Add it temporarily to the\n                // page to compute its style and get its `display` property.\n                this.document.body.appendChild(snippetEl);\n                const snippetStyle = window.getComputedStyle(snippetEl);\n                const isInlineSnippet = snippetStyle.display.includes(\"inline\");\n                snippetEl.remove();\n\n                // Color-customize the snippet dynamic SVGs with the current\n                // theme colors.\n                const dynamicSvgEls = [\n                    ...snippetEl.querySelectorAll(\n                        'img[src^=\"/html_editor/shape/\"], img[src^=\"/web_editor/shape/\"]'\n                    ),\n                ];\n                dynamicSvgEls.forEach((dynamicSvgEl) => {\n                    const colorCustomizedURL = new URL(\n                        dynamicSvgEl.getAttribute(\"src\"),\n                        window.location.origin\n                    );\n                    colorCustomizedURL.searchParams.forEach((value, key) => {\n                        const match = key.match(/^c([1-5])$/);\n                        if (match) {\n                            colorCustomizedURL.searchParams.set(\n                                key,\n                                getCSSVariableValue(\n                                    `o-color-${match[1]}`,\n                                    this.document.defaultView.getComputedStyle(\n                                        this.document.documentElement\n                                    )\n                                )\n                            );\n                        }\n                    });\n                    dynamicSvgEl.src = colorCustomizedURL.pathname + colorCustomizedURL.search;\n                });\n\n                // The dragged element may change while dragging.\n                Object.assign(this.dragState, { draggedEl: snippetEl, snippetEl, snippet });\n\n                // Add the dropzones.\n                const withGrids =\n                    !isSnippetGroup &&\n                    (this.env.editor.config.isMobileView(this.editable) ? \"filterOnly\" : true);\n                const selectors = this.shared.dropzone.getSelectors(snippetEl, false, withGrids);\n                dropzoneEls = this.shared.dropzone.activateDropzones(selectors, {\n                    toInsertInline: isInlineSnippet,\n                });\n\n                this.env.editor.dispatchTo(\"on_snippet_dragged_handlers\", {\n                    snippetEl,\n                    dragState: this.dragState,\n                });\n            },\n            dropzoneOver: ({ dropzone }) => {\n                const dropzoneEl = dropzone.el;\n                if (isSnippetGroup) {\n                    dropzoneEl.classList.add(\"o_dropzone_highlighted\");\n                    this.dragState.currentDropzoneEl = dropzoneEl;\n                    return;\n                }\n                dropzoneEl.after(this.dragState.draggedEl);\n                dropzoneEl.classList.add(\"invisible\");\n                this.dragState.currentDropzoneEl = dropzoneEl;\n\n                this.env.editor.dispatchTo(\"on_snippet_over_dropzone_handlers\", {\n                    snippetEl,\n                    dragState: this.dragState,\n                });\n            },\n            onDrag: ({ x, y }) => {\n                if (!this.dragState.currentDropzoneEl) {\n                    return;\n                }\n\n                this.env.editor.dispatchTo(\"on_snippet_move_handlers\", {\n                    snippetEl,\n                    dragState: this.dragState,\n                    x,\n                    y,\n                });\n            },\n            dropzoneOut: ({ dropzone }) => {\n                const dropzoneEl = dropzone.el;\n                if (isSnippetGroup) {\n                    dropzoneEl.classList.remove(\"o_dropzone_highlighted\");\n                    this.dragState.currentDropzoneEl = null;\n                    return;\n                }\n\n                this.env.editor.dispatchTo(\"on_snippet_out_dropzone_handlers\", {\n                    snippetEl,\n                    dragState: this.dragState,\n                });\n\n                this.dragState.draggedEl.remove();\n                dropzoneEl.classList.remove(\"invisible\");\n                this.dragState.currentDropzoneEl = null;\n            },\n            onDragEnd: async ({ x, y, helper }) => {\n                this.document.body.classList.remove(\"oe_dropzone_active\");\n                let currentDropzoneEl = this.dragState.currentDropzoneEl;\n                const isDroppedOver = !!currentDropzoneEl;\n\n                // If the snippet was dropped outside of a dropzone, find the\n                // dropzone that is the nearest to the dropping point.\n                if (!currentDropzoneEl) {\n                    const blockTabLeft = this.blockTabRef.el.getBoundingClientRect().left;\n                    if (y > 3 && x + helper.getBoundingClientRect().height < blockTabLeft) {\n                        const closestDropzoneEl = closest(dropzoneEls, { x, y });\n                        if (closestDropzoneEl) {\n                            currentDropzoneEl = closestDropzoneEl;\n                        }\n                    }\n                }\n\n                if (currentDropzoneEl) {\n                    let draggedEl = this.dragState.draggedEl;\n                    if (isDroppedOver) {\n                        this.env.editor.dispatchTo(\"on_snippet_dropped_over_handlers\", {\n                            droppedEl: draggedEl,\n                            dragState: this.dragState,\n                        });\n                    } else {\n                        currentDropzoneEl.after(draggedEl);\n                        this.env.editor.dispatchTo(\"on_snippet_dropped_near_handlers\", {\n                            droppedEl: draggedEl,\n                            dropzoneEl: currentDropzoneEl,\n                            dragState: this.dragState,\n                        });\n                    }\n                    // The dragged element may have changed, so get it again.\n                    draggedEl = this.dragState.draggedEl;\n\n                    // In order to mark only the concerned elements as dirty,\n                    // remove the element, then replay the drop after\n                    // re-allowing to mark dirty.\n                    draggedEl.remove();\n\n                    // Undo the changes needed to ease the drag and drop and\n                    // re-allow to mark dirty.\n                    this.dragState.restoreCallbacks.forEach((restore) => restore());\n                    this.dragState.restoreCallbacks = null;\n\n                    // Replay the drop.\n                    currentDropzoneEl.after(draggedEl);\n                    this.shared.dropzone.removeDropzones();\n\n                    // Process the dropped element.\n                    if (!isSnippetGroup) {\n                        await this.processDroppedSnippet(snippetEl);\n                        delete this.cancelDragAndDrop;\n                    } else {\n                        this.shared.operation.next(\n                            async () => {\n                                await this.onSnippetGroupDrop(snippet, snippetEl);\n                            },\n                            {\n                                withLoadingEffect: false,\n                                shouldInterceptClick: true,\n                            }\n                        );\n                    }\n                } else {\n                    this.cancelDragAndDrop();\n                    delete this.cancelDragAndDrop;\n                }\n\n                this.state.ongoingInsertion = false;\n                dragAndDropResolve();\n            },\n        };\n\n        this.draggableComponent = useDragAndDrop(dragAndDropOptions);\n    }\n\n    /**\n     *\n     * @param {HTMLElement} snippetEl\n     */\n    async processDroppedSnippet(snippetEl) {\n        this.updateDroppedSnippet(snippetEl);\n        // Build the snippet.\n        for (const onSnippetDropped of this.env.editor.getResource(\"on_snippet_dropped_handlers\")) {\n            const cancel = await onSnippetDropped({ snippetEl, dragState: this.dragState });\n            // Cancel everything if the resource asked to.\n            if (cancel) {\n                this.cancelDragAndDrop();\n                return;\n            }\n        }\n        this.env.editor.config.updateInvisibleElementsPanel();\n        this.shared.disableSnippets.disableUndroppableSnippets();\n        this.shared.history.addStep();\n    }\n\n    /**\n     * Update the dropped snippet to build & adapt dynamic content right\n     * after adding it to the DOM.\n     *\n     * @param {HTMLElement} snippetEl\n     */\n    updateDroppedSnippet(snippetEl) {\n        // If the snippet is \"drop in only\", remove the attributes that make it\n        // a draggable snippet, so it becomes a simple HTML code.\n        if (snippetEl.classList.contains(\"o_snippet_drop_in_only\")) {\n            snippetEl.classList.remove(\"o_snippet_drop_in_only\");\n            if (snippetEl.classList.length === 0) {\n                snippetEl.removeAttribute(\"class\");\n            }\n            delete snippetEl.dataset.snippet;\n            delete snippetEl.dataset.name;\n        }\n    }\n}\n", "import { Img } from \"@html_builder/core/img\";\nimport { Component, useState, useRef } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useAutofocus } from \"@web/core/utils/hooks\";\n\nexport class CustomInnerSnippet extends Component {\n    static template = \"html_builder.CustomInnerSnippet\";\n    static props = {\n        snippetModel: { type: Object },\n        snippet: { type: Object },\n        onClickHandler: { type: Function },\n        disabledTooltip: { type: String },\n    };\n    static components = { Img };\n\n    setup() {\n        this.renameInputRef = useRef(\"rename-input\");\n        useAutofocus({ refName: \"rename-input\" });\n\n        this.state = useState({ isRenaming: false });\n\n        this.renameButtonTooltip = _t(\"Rename %(snippetTitle)s\", {\n            snippetTitle: this.snippet.title,\n        });\n        this.deleteButtonTooltip = _t(\"Delete %(snippetTitle)s\", {\n            snippetTitle: this.snippet.title,\n        });\n    }\n\n    get snippet() {\n        return this.props.snippet;\n    }\n\n    toggleRenamingState() {\n        this.state.isRenaming = !this.state.isRenaming;\n    }\n\n    onConfirmRename() {\n        this.props.snippetModel.renameCustomSnippet(this.snippet, this.renameInputRef.el.value);\n        this.toggleRenamingState();\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { useOptionsSubEnv } from \"@html_builder/utils/utils\";\nimport { Img } from \"@html_builder/core/img\";\n\nexport class CustomizeComponent extends Component {\n    static template = \"html_builder.CustomizeComponent\";\n    static components = { Img };\n    static props = {\n        editingElements: { type: Array },\n        comp: { type: Function },\n        compProps: { type: Object },\n    };\n\n    setup() {\n        useOptionsSubEnv(() => this.props.editingElements);\n    }\n}\n", "import { Component, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { OptionsContainer } from \"./option_container\";\nimport { useVisibilityObserver } from \"../core/utils\";\nimport { CustomizeComponent } from \"@html_builder/sidebar/customize_component\";\n\nexport class CustomizeTab extends Component {\n    static template = \"html_builder.CustomizeTab\";\n    static components = { CustomizeComponent, OptionsContainer };\n    static props = {\n        currentOptionsContainers: { type: Array, optional: true },\n        snippetModel: { type: Object },\n    };\n    static defaultProps = {\n        currentOptionsContainers: [],\n    };\n\n    setup() {\n        this.state = useState({\n            hasContent: true,\n        });\n        this.customizeComponent = useState(\n            this.env.editor.shared.customizeTab.getCustomizeComponent()\n        );\n        useVisibilityObserver(\"content\", (hasContent) => {\n            this.state.hasContent = hasContent;\n        });\n        onWillUpdateProps((nextProps) => {\n            if (\n                !this.state.hasContent &&\n                nextProps.currentOptionsContainers.length > 0 &&\n                nextProps.currentOptionsContainers !== this.props.currentOptionsContainers\n            ) {\n                // Force a reconsideration of `content`\n                this.state.hasContent = true;\n            }\n        });\n    }\n\n    getCurrentOptionsContainers() {\n        const currentOptionsContainers = this.props.currentOptionsContainers;\n        if (!currentOptionsContainers.length) {\n            return this.env.editor.shared.builderOptions.getPageContainers();\n        }\n        return currentOptionsContainers;\n    }\n}\n", "import { Component, onWillStart, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { getSnippetName, isElementInViewport } from \"@html_builder/utils/utils\";\n\n/**\n * @typedef {((snippetEl: HTMLElement) => void)[]} on_reveal_target_handlers\n */\n\nexport class InvisibleElementsPanel extends Component {\n    static template = \"html_builder.InvisibleElementsPanel\";\n    static props = {\n        invisibleEls: { type: Array },\n        invisibleSelector: { type: String },\n    };\n\n    setup() {\n        this.state = useState({ invisibleEntries: null });\n\n        onWillStart(() => this.updateInvisibleElementsPanel(this.props.invisibleEls));\n\n        onWillUpdateProps((nextProps) => {\n            const { invisibleEls, invisibleSelector } = nextProps;\n            this.updateInvisibleElementsPanel(invisibleEls, invisibleSelector);\n        });\n    }\n\n    get shared() {\n        return this.env.editor.shared;\n    }\n\n    updateInvisibleElementsPanel(invisibleEls, invisibleSelector = this.props.invisibleSelector) {\n        // descendantPerSnippet: a map with its keys set to invisible\n        // snippets that have invisible descendants. The value corresponding\n        // to an invisible snippet element is a list filled with all its\n        // descendant invisible snippets except those that have a closer\n        // invisible snippet ancestor.\n        const descendantPerSnippet = new Map();\n        // Filter the invisibleEls to only keep the root snippets\n        // and create the map (\"descendantPerSnippet\") of the snippets and\n        // their descendant snippets.\n        const rootInvisibleSnippetEls = invisibleEls.filter((invisibleSnippetEl) => {\n            const ancestorInvisibleEl = invisibleSnippetEl.parentElement.closest(invisibleSelector);\n            if (!ancestorInvisibleEl) {\n                return true;\n            }\n            const descendantSnippets = descendantPerSnippet.get(ancestorInvisibleEl) || [];\n            descendantPerSnippet.set(ancestorInvisibleEl, [\n                ...descendantSnippets,\n                invisibleSnippetEl,\n            ]);\n            return false;\n        });\n        // Insert all the invisible snippets contained in \"snippetEls\" as\n        // well as their descendants in the \"parentEl\" element. If\n        // \"snippetEls\" is set to \"rootInvisibleSnippetEls\" and \"parentEl\"\n        // is set to \"$invisibleDOMPanelEl[0]\", then fills the right\n        // invisible panel like this:\n        // rootInvisibleSnippet\n        //     \u2514 descendantInvisibleSnippet\n        //          \u2514 descendantOfDescendantInvisibleSnippet\n        //               \u2514 etc...\n        const createInvisibleEntries = (snippetEls, parentEl = null) =>\n            snippetEls.map((snippetEl) => {\n                const descendantSnippetEls = descendantPerSnippet.get(snippetEl);\n                // An element is considered as \"RootParent\" if it has one or\n                // more invisible descendants but is not a descendant.\n                const invisibleElement = {\n                    snippetEl: snippetEl,\n                    name: getSnippetName(snippetEl),\n                    isRootParent: !parentEl && !!descendantSnippetEls,\n                    isDescendant: !!parentEl,\n                    isVisible: snippetEl.dataset.invisible !== \"1\",\n                    children: [],\n                    parentEl,\n                };\n                if (descendantSnippetEls) {\n                    invisibleElement.children = createInvisibleEntries(\n                        descendantSnippetEls,\n                        invisibleElement\n                    );\n                }\n                return invisibleElement;\n            });\n        this.state.invisibleEntries = createInvisibleEntries(rootInvisibleSnippetEls);\n    }\n\n    toggleElementVisibility(invisibleEntry) {\n        const snippetEl = invisibleEntry.snippetEl;\n        if (invisibleEntry.isVisible) {\n            // Toggle the entry visibility to \"Hide\".\n            invisibleEntry.isVisible = false;\n            this.shared.visibility.toggleTargetVisibility(snippetEl, false);\n            this.shared.builderOptions.deactivateContainers();\n        } else {\n            // Toggle the entry visibility to \"Show\".\n            invisibleEntry.isVisible = true;\n            this.shared.visibility.toggleTargetVisibility(snippetEl, true);\n            this.env.editor.dispatchTo(\"on_reveal_target_handlers\", snippetEl);\n            this.shared.builderOptions.updateContainers(snippetEl);\n            // Scroll to the target if not visible.\n            if (!isElementInViewport(snippetEl) && !snippetEl.matches(\".s_popup\")) {\n                snippetEl.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n            }\n        }\n        this.shared.disableSnippets.disableUndroppableSnippets();\n    }\n}\n", "import { BorderConfigurator } from \"../plugins/border_configurator_option\";\nimport { ShadowOption } from \"../plugins/shadow_option\";\nimport { getSnippetName, useOptionsSubEnv } from \"@html_builder/utils/utils\";\nimport { onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useOperation } from \"../core/operation_plugin\";\nimport {\n    BaseOptionComponent,\n    useApplyVisibility,\n    useGetItemValue,\n    useVisibilityObserver,\n} from \"../core/utils\";\n\nexport class OptionsContainer extends BaseOptionComponent {\n    static template = \"html_builder.OptionsContainer\";\n    static dependencies = [\"builderOptions\", \"overlayButtons\", \"builderOverlay\", \"remove\", \"clone\"];\n    static components = {\n        BorderConfigurator,\n        ShadowOption,\n    };\n    static props = {\n        snippetModel: { type: Object },\n        options: { type: Array },\n        editingElement: true, // HTMLElement from iframe\n        isRemovable: false,\n        removeDisabledReason: { type: String, optional: true },\n        isClonable: false,\n        cloneDisabledReason: { type: String, optional: true },\n        optionTitleComponents: { type: Array, optional: true },\n        containerTopButtons: { type: Array },\n        containerTitle: { type: Object, optional: true },\n        headerMiddleButtons: { type: Array, optional: true },\n    };\n    static defaultProps = {\n        containerTitle: {},\n        headerMiddleButtons: [],\n        optionTitleComponents: [],\n    };\n\n    setup() {\n        useOptionsSubEnv(() => [this.props.editingElement]);\n        super.setup();\n        this.notification = useService(\"notification\");\n        this.getItemValue = useGetItemValue();\n        useVisibilityObserver(\"content\", useApplyVisibility(\"root\"));\n\n        this.callOperation = useOperation();\n\n        this.hasGroup = {};\n        onWillStart(async () => {\n            await this.updateAccessGroup(this.props.options);\n        });\n        onWillUpdateProps(async (nextProps) => {\n            await this.updateAccessGroup(nextProps.options);\n        });\n    }\n\n    async updateAccessGroup(options) {\n        const proms = [];\n        const groups = [...new Set(options.flatMap((o) => o.groups || []))];\n        for (const group of groups) {\n            proms.push(\n                user.hasGroup(group).then((result) => {\n                    this.hasGroup[group] = result;\n                })\n            );\n        }\n        await Promise.all(proms);\n    }\n\n    hasAccess(groups) {\n        if (!groups) {\n            return true;\n        }\n        return groups.every((group) => this.hasGroup[group]);\n    }\n\n    get title() {\n        let title;\n        for (const option of this.props.options) {\n            if (option.getSnippetTitle) {\n                title = option.getSnippetTitle.call(this);\n                continue;\n            }\n            title = option.title || title;\n        }\n        const titleExtraInfo = this.props.containerTitle.getTitleExtraInfo\n            ? this.props.containerTitle.getTitleExtraInfo(this.props.editingElement)\n            : \"\";\n\n        return (title || getSnippetName(this.env.getEditingElement())) + titleExtraInfo;\n    }\n\n    selectElement() {\n        this.dependencies.builderOptions.updateContainers(this.props.editingElement);\n    }\n\n    toggleOverlayPreview(el, show) {\n        if (show) {\n            this.dependencies.overlayButtons.hideOverlayButtons();\n            this.dependencies.builderOverlay.showOverlayPreview(el);\n        } else {\n            this.dependencies.overlayButtons.showOverlayButtons();\n            this.dependencies.builderOverlay.hideOverlayPreview(el);\n        }\n    }\n\n    onPointerEnter() {\n        this.toggleOverlayPreview(this.props.editingElement, true);\n    }\n\n    onPointerLeave() {\n        this.toggleOverlayPreview(this.props.editingElement, false);\n    }\n\n    // Actions of the buttons in the title bar.\n    removeElement() {\n        this.callOperation(() => {\n            this.dependencies.remove.removeElement(this.props.editingElement);\n        });\n    }\n\n    cloneElement() {\n        this.callOperation(async () => {\n            await this.dependencies.clone.cloneElement(this.props.editingElement, {\n                activateClone: false,\n            });\n        });\n    }\n\n    isLegacyOption(option) {\n        return typeof option === \"object\";\n    }\n}\n", "import { Img } from \"@html_builder/core/img\";\nimport { Component } from \"@odoo/owl\";\n\nexport class Snippet extends Component {\n    static template = \"html_builder.Snippet\";\n    static components = { Img };\n    static props = {\n        snippetModel: { type: Object },\n        snippet: { type: Object },\n        onClickHandler: { type: Function },\n        disabledTooltip: { type: String },\n    };\n\n    get snippet() {\n        return this.props.snippet;\n    }\n\n    onInstallableHover(ev) {\n        if (this.snippet.isInstallable) {\n            ev.currentTarget\n                .querySelector(\".o_install_btn\")\n                .classList.toggle(\"visually-hidden-focusable\", ev.type !== \"mouseover\");\n        }\n    }\n\n    onClickInstall() {\n        this.props.snippetModel.installSnippetModule(\n            this.props.snippet,\n            this.env.editor.config.installSnippetModule\n        );\n    }\n}\n", "import { Component, onMounted, onWillUnmount, onWillRender, useRef, useState } from \"@odoo/owl\";\nimport { loadBundle, loadCSS } from \"@web/core/assets\";\nimport { isBrowserFirefox } from \"@web/core/browser/feature_detection\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { getFirstAndLastTabableElements } from \"@web/core/ui/ui_service\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\nimport { SnippetViewer } from \"./snippet_viewer\";\n\n/**\n * @typedef {((arg: { iframe: HTMLIFrameElement }) => void)[]} snippet_preview_dialog_stylesheets_handlers\n * @typedef {string[]} snippet_preview_dialog_bundles\n */\n\nexport class AddSnippetDialog extends Component {\n    static template = \"html_builder.AddSnippetDialog\";\n    static components = { Dialog };\n    static props = {\n        selectedSnippet: { type: Object },\n        selectSnippet: { type: Function },\n        snippetModel: { type: Object },\n        close: { type: Function },\n        installSnippetModule: { type: Function },\n        editor: { type: Object },\n    };\n\n    setup() {\n        this.iframeRef = useRef(\"iframe\");\n        this.modalRef = useChildRef();\n        this.state = useState({\n            search: \"\",\n            groupSelected: this.props.selectedSnippet.groupName,\n            showIframe: false,\n            hasNoSearchResults: false,\n        });\n        this.snippetViewerProps = {\n            state: this.state,\n            hasSearchResults: (has) => {\n                this.state.hasNoSearchResults = !has;\n            },\n            selectSnippet: (...args) => {\n                this.props.selectSnippet(...args);\n                this.props.close();\n            },\n            snippetModel: this.props.snippetModel,\n            installSnippetModule: this.props.installSnippetModule,\n            frontendDirection: this.props.editor.editable.classList.contains(\"o_rtl\")\n                ? \"rtl\"\n                : \"ltr\",\n        };\n\n        let root;\n        onMounted(async () => {\n            const isFirefox = isBrowserFirefox();\n            if (isFirefox) {\n                // Make sure empty preview iframe is loaded.\n                // This event is never triggered on Chrome.\n                await new Promise((resolve) => {\n                    this.iframeRef.el.addEventListener(\"load\", resolve, { once: true });\n                });\n            }\n\n            const iframeDocument = this.iframeRef.el.contentDocument;\n            iframeDocument.body.parentElement.classList.add(\"o_add_snippets_preview\");\n            iframeDocument.body.style.setProperty(\"direction\", localization.direction);\n            iframeDocument.body.tabIndex = \"-1\";\n            iframeDocument.addEventListener(\"keydown\", this.onIframeDocumentKeydown.bind(this));\n\n            root = this.__owl__.app.createRoot(SnippetViewer, {\n                props: this.snippetViewerProps,\n            });\n            root.mount(iframeDocument.body);\n\n            await this.insertStyle();\n            this.insertColorScheme();\n            this.state.showIframe = true;\n        });\n\n        onWillRender(() => {\n            if (!this.props.snippetModel.hasCustomGroup && this.state.groupSelected === \"custom\") {\n                this.state.groupSelected = this.props.snippetModel.snippetGroups[0].groupName;\n            }\n        });\n\n        onWillUnmount(() => {\n            root.destroy();\n        });\n    }\n\n    /**\n     * Loads and injects the required styles into the iframe's <head>.\n     * The URL for web.assets_frontend CSS bundle is retrieved from the editor\n     * document to ensure consistency, especially when using the RTL version.\n     */\n    async insertStyle() {\n        const loadCSSBundleFromEditor = (bundleName, loadOptions) => {\n            const cssLinkEl = this.props.editor.document.head.querySelector(\n                `link[type=\"text/css\"][href*=\"/${bundleName}.\"]`\n            );\n            if (cssLinkEl) {\n                return loadCSS(cssLinkEl.getAttribute(\"href\"), loadOptions);\n            }\n            return loadBundle(bundleName, loadOptions);\n        };\n        this.props.editor.dispatchTo(\"snippet_preview_dialog_stylesheets_handlers\", {\n            iframe: this.iframeRef.el,\n        });\n        const editorPreviewAssetsBundles = this.props.editor.getResource(\n            \"snippet_preview_dialog_bundles\"\n        );\n        const loadOptions = { targetDoc: this.iframeRef.el.contentDocument, js: false };\n        await Promise.all([\n            ...editorPreviewAssetsBundles.map((assetsBundle) =>\n                loadCSSBundleFromEditor(assetsBundle, loadOptions)\n            ),\n            loadBundle(\"html_builder.iframe_add_dialog\", loadOptions),\n        ]);\n    }\n\n    get snippetGroups() {\n        return this.props.snippetModel.snippetGroups.filter(\n            (snippetGroup) => !snippetGroup.moduleId\n        );\n    }\n\n    selectGroup(snippetGroup) {\n        this.state.groupSelected = snippetGroup.groupName;\n        const iframeDocument = this.iframeRef.el.contentDocument;\n        iframeDocument.body.scrollTop = 0;\n    }\n\n    /**\n     * Retrieves the color-scheme cookie and injects it into the iframe's\n     * <head> and add a custom class. This is necessary to allow the dark mode\n     * to be handled correctly across browsers.\n     */\n    insertColorScheme() {\n        const colorScheme = cookie.get(\"color_scheme\") || \"light\";\n        const metaElement = document.createElement(\"meta\");\n        const iframeDocument = this.iframeRef.el.contentDocument;\n        metaElement.setAttribute(\"name\", \"color-scheme\");\n        metaElement.content = colorScheme;\n        iframeDocument.head.appendChild(metaElement);\n        iframeDocument.body.parentElement.classList.add(\"o_add_snippets_preview--\" + colorScheme);\n    }\n\n    /**\n     * Handles the tablist navigation.\n     *\n     * @param {KeyboardEvent} ev\n     */\n    onTabKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (![\"arrowleft\", \"arrowright\", \"arrowdown\", \"arrowup\"].includes(hotkey)) {\n            return;\n        }\n        if ([\"arrowleft\", \"arrowup\"].includes(hotkey)) {\n            ev.currentTarget.previousElementSibling?.focus();\n        } else {\n            ev.currentTarget.nextElementSibling?.focus();\n        }\n    }\n    /**\n     * The mix of focused elements within the dialog and within the iframe does\n     * not work well with the `useActiveElement` standard focus trap. This\n     * listener ensures the cycle is well supported.\n     *\n     * @param {KeyboardEvent} ev\n     */\n    onIframeDocumentKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (![\"tab\", \"shift+tab\"].includes(hotkey)) {\n            return;\n        }\n        const [, lastTabableElInIframe] = getFirstAndLastTabableElements(ev.currentTarget);\n        if (hotkey === \"tab\" && lastTabableElInIframe === ev.target) {\n            const [firstTabableElInDialog] = getFirstAndLastTabableElements(this.modalRef.el);\n            firstTabableElInDialog.focus();\n            ev.preventDefault();\n            ev.stopPropagation();\n        } else if (hotkey === \"shift+tab\" && ev.target.tagName === \"BODY\") {\n            lastTabableElInIframe.focus();\n            ev.preventDefault();\n            ev.stopPropagation();\n        }\n    }\n}\n", "import { useState } from \"@odoo/owl\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nexport class InputConfirmationDialog extends ConfirmationDialog {\n    static template = \"html_builder.InputConfirmationDialog\";\n\n    static props = {\n        ...ConfirmationDialog.props,\n        inputLabel: { type: String, optional: true },\n        defaultValue: { type: String, optional: true },\n    };\n\n    setup() {\n        super.setup();\n        this.inputState = useState({\n            value: this.props.defaultValue,\n        });\n    }\n\n    execButton(callback) {\n        return super.execButton((...args) => callback?.(...args, this.inputState.value));\n    }\n}\n", "import { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { Reactive } from \"@web/core/utils/reactive\";\nimport { AddSnippetDialog } from \"./add_snippet_dialog\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { markup, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class SnippetModel extends Reactive {\n    constructor(services, { snippetsName, context }) {\n        super();\n        this.orm = services.orm;\n        this.dialog = services.dialog;\n        this.snippetsName = snippetsName;\n        this.context = context;\n        this.loadProm = null;\n\n        this.snippetsByCategory = {\n            snippet_groups: [],\n            snippet_custom: [],\n            snippet_structure: [],\n            snippet_content: [],\n            snippet_custom_content: [],\n        };\n    }\n\n    get hasCustomGroup() {\n        return !!this.snippetsByCategory.snippet_custom.length;\n    }\n\n    get snippetGroups() {\n        const snippetGroups = this.snippetsByCategory.snippet_groups;\n        if (this.hasCustomGroup) {\n            return snippetGroups;\n        }\n        return snippetGroups.filter((snippet) => snippet.groupName !== \"custom\");\n    }\n\n    get snippetStructures() {\n        return [\n            ...this.snippetsByCategory.snippet_structure,\n            ...this.snippetsByCategory.snippet_custom,\n        ];\n    }\n\n    get snippetInnerContents() {\n        return this.snippetsByCategory.snippet_content;\n    }\n\n    get hasCustomInnerContents() {\n        return !!this.snippetsByCategory.snippet_custom_content.length;\n    }\n\n    get snippetCustomInnerContents() {\n        return this.snippetsByCategory.snippet_custom_content;\n    }\n\n    isCustomInnerContent(customSnippetName) {\n        return !!this.snippetsByCategory.snippet_content.find(\n            (snippet) => snippet.name === customSnippetName\n        );\n    }\n\n    isCustomStructure(customSnippetName) {\n        return !!this.snippetsByCategory.snippet_structure.find(\n            (snippet) => snippet.name === customSnippetName\n        );\n    }\n\n    /**\n     * Checks if the given element is an inner content snippet.\n     *\n     * @param {HTMLElement} el the element\n     * @returns {Boolean}\n     */\n    isInnerContent(el) {\n        const snippetName = el.dataset.snippet;\n        if (!snippetName) {\n            return false;\n        }\n        return !!this.snippetsByCategory.snippet_content.find(\n            (snippet) => snippet.name === snippetName\n        );\n    }\n\n    getSnippet(category, id) {\n        return this.snippetsByCategory[category].find((snippet) => snippet.id === id);\n    }\n\n    getSnippetByName(category, name) {\n        return this.snippetsByCategory[category].find((snippet) => snippet.name === name);\n    }\n\n    installSnippetModule(snippet, installSnippetModule) {\n        const bodyText = _t(\"Do you want to install %s App?\", snippet.moduleDisplayName);\n        const linkText = _t(\"More info about this app.\");\n        const linkUrl =\n            \"/odoo/action-base.open_module_tree/\" + encodeURIComponent(snippet.moduleId);\n\n        this.dialog.add(ConfirmationDialog, {\n            title: _t(\"Install %s\", snippet.moduleDisplayName),\n            body: markup`${bodyText}\\n<a href=\"${linkUrl}\" target=\"_blank\"><i class=\"oi oi-arrow-right me-1\"></i>${linkText}</a>`,\n            confirm: async () => installSnippetModule(snippet),\n            confirmLabel: _t(\"Save and Install\"),\n            cancel: () => {},\n        });\n    }\n\n    /**\n     * Opens the snippet dialog on the group of the given snippet object and\n     * allows to specify its behaviour when selecting a snippet and when closing\n     * it.\n     *\n     * @param {Object} snippet the snippet object\n     * @param {Object} - `onSelect` called when a snippet is selected. Must return\n     *     an HTMLElement.\n     *                 - `onClose` called when the dialog is closed.\n     * @param {Object} editor\n     */\n    openSnippetDialog(snippet, { onSelect, onClose }, editor) {\n        this.dialog.add(\n            AddSnippetDialog,\n            {\n                selectedSnippet: snippet,\n                snippetModel: this,\n                selectSnippet: (...args) => {\n                    const newSnippetEl = onSelect(...args);\n                    this.updateSnippetContent(newSnippetEl);\n                },\n                installSnippetModule: editor.config.installSnippetModule,\n                editor,\n            },\n            { onClose }\n        );\n    }\n\n    load() {\n        if (!this.loadProm) {\n            this.loadProm = (async () => {\n                const context = { ...this.context, rendering_bundle: true };\n                if (context.user_lang) {\n                    context.lang = this.context.user_lang;\n                    context.snippet_lang = this.context.lang;\n                }\n                const html = await this.orm.silent.call(\n                    \"ir.ui.view\",\n                    \"render_public_asset\",\n                    [this.snippetsName, {}],\n                    { context }\n                );\n                this.snippetsDocument = new DOMParser().parseFromString(html, \"text/html\");\n                const processors = registry.category(\"html_builder.snippetsPreprocessor\").getAll();\n                for (const processor of Object.values(processors)) {\n                    processor(this.snippetsName, this.snippetsDocument);\n                }\n                this.computeSnippetTemplates(this.snippetsDocument);\n                this.setSnippetName(this.snippetsDocument);\n            })();\n        }\n        return this.loadProm;\n    }\n\n    /**\n     * Reloads the snippet data, optionally updating the context.\n     *\n     * @param {Object} context - Optional context to override or extend the\n     *                           current context.\n     * @returns {Promise<void>} A promise that resolves once the snippets are\n     *                          reloaded.\n     */\n    reload(context = {}) {\n        this.loadProm = null;\n        this.context = {\n            ...this.context,\n            ...context,\n        };\n        return this.load();\n    }\n\n    computeSnippetTemplates(snippetsDocument) {\n        const snippetsBody = snippetsDocument.body;\n        this.snippetsByCategory = {};\n        for (const snippetCategory of snippetsBody.querySelectorAll(\"snippets\")) {\n            const snippets = [];\n            for (const snippetEl of snippetCategory.children) {\n                const snippet = {\n                    id: uniqueId(),\n                    title: snippetEl.getAttribute(\"name\"),\n                    name: snippetEl.children[0].dataset.snippet,\n                    content: snippetEl.children[0],\n                    viewId: parseInt(snippetEl.dataset.oeSnippetId),\n                    key: snippetEl.dataset.oeSnippetKey,\n                    thumbnailSrc: snippetEl.dataset.oeThumbnail,\n                    imagePreviewSrc: snippetEl.dataset.oImagePreview,\n                    isCustom: false,\n                    label: this.getSnippetLabel(snippetEl),\n                    isDisabled: false,\n                    forbidSanitize: false,\n                    gridColumnSpan: 0,\n                };\n                const moduleId = snippetEl.dataset.moduleId;\n                if (moduleId) {\n                    Object.assign(snippet, {\n                        moduleId,\n                        isInstallable: !!moduleId,\n                        moduleDisplayName: snippetEl.dataset.moduleDisplayName,\n                    });\n                }\n                if (snippetEl.dataset.oeForbidSanitize) {\n                    Object.assign(snippet, { forbidSanitize: snippetEl.dataset.oeForbidSanitize });\n                }\n                if (snippetEl.dataset.oGridColumnSpan) {\n                    snippet.gridColumnSpan = parseInt(snippetEl.dataset.oGridColumnSpan);\n                }\n                switch (snippetCategory.id) {\n                    case \"snippet_groups\":\n                        snippet.groupName = snippetEl.dataset.oSnippetGroup;\n                        break;\n                    case \"snippet_structure\":\n                        snippet.groupName = snippetEl.dataset.oGroup;\n                        snippet.keyWords = snippetEl.dataset.oeKeywords;\n                        break;\n                    case \"snippet_custom\":\n                        snippet.groupName = \"custom\";\n                        snippet.isCustom = true;\n                        break;\n                }\n                snippets.push(snippet);\n            }\n            this.snippetsByCategory[snippetCategory.id] = snippets;\n        }\n\n        // Extract the custom inner content from the custom snippets and remove\n        // those whose module is not installed.\n        const customInnerContent = [];\n        const customSnippets = this.snippetsByCategory.snippet_custom;\n        for (let i = customSnippets.length - 1; i >= 0; i--) {\n            const snippet = customSnippets[i];\n            const customSnippetName = snippet.name.startsWith(\"s_button_\")\n                ? \"s_button\"\n                : snippet.name;\n            if (this.isCustomInnerContent(customSnippetName)) {\n                customInnerContent.unshift(snippet);\n                customSnippets.splice(i, 1);\n            } else if (!this.isCustomStructure(customSnippetName)) {\n                // If no structure snippet could be found, it means that the\n                // module is not installed (i.e. the original snippet has no\n                // `data-snippet` attribute).\n                customSnippets.splice(i, 1);\n            }\n        }\n        this.snippetsByCategory[\"snippet_custom_content\"] = customInnerContent;\n    }\n\n    async deleteCustomSnippet(snippet) {\n        return new Promise((resolve) => {\n            const message = _t(\"Are you sure you want to delete the block %s?\", snippet.title);\n            this.dialog.add(\n                ConfirmationDialog,\n                {\n                    body: message,\n                    confirm: async () => {\n                        const isInnerContent =\n                            this.snippetsByCategory.snippet_custom_content.includes(snippet);\n                        const snippetCustom = isInnerContent\n                            ? this.snippetsByCategory.snippet_custom_content\n                            : this.snippetsByCategory.snippet_custom;\n                        const index = snippetCustom.findIndex((s) => s.id === snippet.id);\n                        if (index > -1) {\n                            snippetCustom.splice(index, 1);\n                        }\n                        await this.orm.call(\"ir.ui.view\", \"delete_snippet\", [], {\n                            view_id: snippet.viewId,\n                            template_key: this.snippetsName,\n                        });\n                    },\n                    cancel: () => {},\n                    confirmLabel: _t(\"Yes\"),\n                    cancelLabel: _t(\"No\"),\n                },\n                {\n                    onClose: resolve,\n                }\n            );\n        });\n    }\n\n    async renameCustomSnippet(snippet, newName) {\n        if (newName === snippet.title) {\n            return;\n        }\n        snippet.title = newName;\n        for (const snippetEl of this.snippetsDocument.body.querySelectorAll(\n            `snippets#snippet_custom > [data-oe-snippet-key = ${snippet.key}]`\n        )) {\n            snippetEl.setAttribute(\"name\", newName);\n            snippetEl.children[0].dataset[\"name\"] = newName;\n        }\n        await this.orm.call(\"ir.ui.view\", \"rename_snippet\", [], {\n            name: newName,\n            view_id: snippet.viewId,\n            template_key: this.snippetsName,\n        });\n    }\n\n    setSnippetName(snippetsDocument) {\n        // TODO: this should probably be done in py\n        for (const snippetEl of snippetsDocument.body.querySelectorAll(\"snippets > *\")) {\n            snippetEl.children[0].dataset[\"name\"] = snippetEl.getAttribute(\"name\");\n        }\n    }\n\n    /**\n     * Returns the original snippet object based on the given `data-snippet`\n     * attribute.\n     *\n     * @param {String} snippetKey the `data-snippet` attribute of the snippet.\n     * @returns {Object}\n     */\n    getOriginalSnippet(snippetKey) {\n        if (!snippetKey) {\n            return;\n        }\n        return [...this.snippetStructures, ...this.snippetInnerContents].find(\n            (snippet) => snippet.name === snippetKey\n        );\n    }\n\n    /**\n     * Returns the snippet thumbnail URL.\n     *\n     * @param {String} snippetKey the `data-snippet` attribute of the snippet.\n     * @returns\n     */\n    getSnippetThumbnailURL(snippetKey) {\n        const originalSnippet = this.getOriginalSnippet(snippetKey);\n        return originalSnippet.thumbnailSrc;\n    }\n\n    /**\n     * Removes the previews from the given snippet.\n     *\n     * @param {HTMLElement} snippetEl\n     */\n    updateSnippetContent(snippetEl) {\n        snippetEl.querySelectorAll(\".s_dialog_preview\").forEach((el) => el.remove());\n    }\n\n    /**\n     * Saves the given snippet as a custom one and reloads all the snippets\n     * to have access to it directly.\n     *\n     * @param {HTMLElement} snippetEl the snippet we want to save\n     * @param {Array<Function>} cleanForSaveHandlers all the hanlders of the\n     *     `clean_for_save_handlers` resources\n     * @param {Function} wrapWithSaveSnippetHandlers a function that processes the snippet\n     * before and/or after the cloning. E.g. stopping the interactions before\n     * cloning and restarting them after cloning.\n     * @returns\n     */\n    saveSnippet(\n        snippetEl,\n        cleanForSaveHandlers,\n        wrapWithSaveSnippetHandlers = (_, callback) => callback()\n    ) {\n        return new Promise((resolve) => {\n            this.dialog.add(\n                ConfirmationDialog,\n                {\n                    title: _t(\"Create a custom snippet\"),\n                    body: _t(\"Do you want to save this snippet as a custom one?\"),\n                    confirmLabel: _t(\"Save\"),\n                    cancel: () => resolve(false),\n                    confirm: async () => {\n                        const isButton = snippetEl.matches(\"a.btn\");\n                        const snippetKey = isButton ? \"s_button\" : snippetEl.dataset.snippet;\n                        const thumbnailURL = this.getSnippetThumbnailURL(snippetKey);\n\n                        const snippetCopyEl = await wrapWithSaveSnippetHandlers(snippetEl, () =>\n                            snippetEl.cloneNode(true)\n                        );\n\n                        // \"CleanForSave\" the snippet copy (only its children in\n                        // the case of a popup, or it will be saved as invisible\n                        // and will not be visible in the \"add snippet\" dialog).\n                        const rootEl = snippetEl.matches(\".s_popup\")\n                            ? snippetCopyEl.firstElementChild\n                            : snippetCopyEl;\n                        cleanForSaveHandlers.forEach((handler) => handler({ root: rootEl }));\n\n                        const defaultSnippetName = isButton\n                            ? _t(\"Custom Button\")\n                            : _t(\"Custom %s\", snippetEl.dataset.name);\n                        snippetCopyEl.classList.add(\"s_custom_snippet\");\n                        delete snippetCopyEl.dataset.name;\n                        if (isButton) {\n                            snippetCopyEl.classList.remove(\"mb-2\");\n                            snippetCopyEl.classList.add(\n                                \"o_snippet_drop_in_only\",\n                                \"s_custom_button\"\n                            );\n                        }\n\n                        const editableParentEl = snippetEl.closest(\n                            \"[data-oe-model][data-oe-field][data-oe-id]\"\n                        );\n                        const context = {\n                            ...this.context,\n                            model: editableParentEl.dataset.oeModel,\n                            field: editableParentEl.dataset.oeField,\n                            resId: editableParentEl.dataset.oeId,\n                        };\n                        const savedName = await this.orm.call(\"ir.ui.view\", \"save_snippet\", [], {\n                            name: defaultSnippetName,\n                            arch: snippetCopyEl.outerHTML,\n                            template_key: this.snippetsName,\n                            snippet_key: snippetKey,\n                            thumbnail_url: thumbnailURL,\n                            context,\n                        });\n\n                        // Reload the snippets so the sidebar is up to date.\n                        await this.reload();\n                        resolve(savedName);\n                    },\n                },\n                { onClose: () => resolve(false) }\n            );\n        });\n    }\n\n    /**\n     * Gets the label of the snippet.\n     *\n     * @param {HTMLElement} snippetEl\n     * @returns {String}\n     */\n    getSnippetLabel(snippetEl) {\n        return snippetEl.dataset.oLabel;\n    }\n}\n\nexport const snippetService = {\n    dependencies: [\"orm\", \"dialog\"],\n\n    start(env, { orm, dialog }) {\n        const services = { orm, dialog };\n        const context = {\n            lang: user.context.lang, // will be overridden by each module through reload().\n            user_lang: user.context.lang,\n        };\n\n        const snippetModelsMap = new Map();\n        const getSnippetModel = (snippetsName) => {\n            if (snippetModelsMap.has(snippetsName)) {\n                return snippetModelsMap.get(snippetsName);\n            }\n            snippetModelsMap.set(\n                snippetsName,\n                new SnippetModel(services, {\n                    snippetsName,\n                    context,\n                })\n            );\n            return snippetModelsMap.get(snippetsName);\n        };\n\n        return { getSnippetModel };\n    },\n};\n\nregistry.category(\"services\").add(\"html_builder.snippets\", snippetService);\n\nexport function useSnippets(snippetsName) {\n    const snippetsService = useService(\"html_builder.snippets\");\n    return useState(snippetsService.getSnippetModel(snippetsName));\n}\n", "import { Component, markup, useRef } from \"@odoo/owl\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { InputConfirmationDialog } from \"./input_confirmation_dialog\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\n\nexport class SnippetViewer extends Component {\n    static template = \"html_builder.SnippetViewer\";\n    static props = {\n        state: { type: Object },\n        selectSnippet: { type: Function },\n        hasSearchResults: Function,\n        snippetModel: { type: Object },\n        installSnippetModule: { type: Function },\n        frontendDirection: { type: String },\n    };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.content = useRef(\"content\");\n    }\n\n    getRenameBtnLabel(snippetName) {\n        return _t(\"Rename %(snippetName)s\", { snippetName });\n    }\n    getDeleteBtnLabel(snippetName) {\n        return _t(\"Delete %(snippetName)s\", { snippetName });\n    }\n\n    onClickRename(snippet) {\n        this.dialog.add(InputConfirmationDialog, {\n            title: _t(\"Rename the block\"),\n            inputLabel: _t(\"Name\"),\n            defaultValue: snippet.title,\n            confirmLabel: _t(\"Save\"),\n            confirm: (inputValue) => {\n                this.props.snippetModel.renameCustomSnippet(snippet, inputValue);\n            },\n            cancelLabel: _t(\"Discard\"),\n            cancel: () => {},\n        });\n    }\n\n    onClickDelete(snippet) {\n        this.props.snippetModel.deleteCustomSnippet(snippet);\n    }\n\n    getSnippetColumns() {\n        const snippets = this.getSelectedSnippets();\n\n        const columns = [[], []];\n        for (const index in snippets) {\n            if (index % 2 === 0) {\n                columns[0].push(snippets[index]);\n            } else {\n                columns[1].push(snippets[index]);\n            }\n        }\n        let numResults = 0;\n        for (const column of columns) {\n            numResults += column.length;\n        }\n        this.props.hasSearchResults(numResults > 0);\n        return columns;\n    }\n\n    onClick(snippet) {\n        if (snippet.moduleId) {\n            this.props.snippetModel.installSnippetModule(snippet, this.props.installSnippetModule);\n        } else {\n            this.props.selectSnippet(snippet);\n        }\n    }\n\n    onPreviewKeydown(ev, snippet) {\n        const hotkey = getActiveHotkey(ev);\n        if (hotkey === \"enter\" || hotkey === \"space\") {\n            this.onClick(snippet);\n        }\n    }\n\n    getContent(elem) {\n        return markup(elem.outerHTML);\n    }\n\n    getButtonInstallName(snippet) {\n        return _t(\"Install %s\", snippet.title);\n    }\n\n    getSelectedSnippets() {\n        const snippetStructures = this.props.snippetModel.snippetStructures.filter(\n            (snippet) => !snippet.isExcluded && !snippet.isDisabled\n        );\n        if (this.previousSearch !== this.props.state.search) {\n            this.previousSearch = this.props.state.search;\n            if (this.content.el) {\n                this.content.el.ownerDocument.body.scrollTop = 0;\n            }\n        }\n        const getClasses = (snippet) => {\n            const classes = new Set();\n            const elements = [snippet.content, ...snippet.content.querySelectorAll(\"*\")];\n            for (const el of elements) {\n                for (const className of el.classList) {\n                    if (className.startsWith(\"s_\")) {\n                        classes.add(className);\n                    }\n                }\n            }\n            return Array.from(classes);\n        };\n        if (this.props.state.search) {\n            return fuzzyLookup(this.props.state.search, snippetStructures, (snippet) => [\n                snippet.title || \"\",\n                snippet.name || \"\",\n                ...(snippet.keyWords?.split(\",\") || \"\"),\n                ...getClasses(snippet),\n            ]);\n        }\n\n        return snippetStructures.filter(\n            (snippet) => snippet.groupName === this.props.state.groupSelected\n        );\n    }\n}\n", "/**\n * Calculates the number of columns for the mobile or desktop version.\n * If all elements don't have the same size, returns \"custom\".\n *\n * @private\n * @param {HTMLCollection} columnEls - elements in the .row container\n * @param {boolean} isMobile\n * @param {String} mobileBreakpoint - bootstrap breakpoint (sm - md - lg)\n * @returns {integer|string} number of columns or \"custom\"\n */\nexport function getNbColumns(columnEls, isMobile, mobileBreakpoint) {\n    if (!columnEls?.length) {\n        return 0;\n    }\n    if (areColsCustomized(columnEls, isMobile, mobileBreakpoint)) {\n        return \"custom\";\n    }\n\n    const resolutionModifier = isMobile ? \"\" : `${mobileBreakpoint}-`;\n    const colRegex = new RegExp(`(?:^|\\\\s+)col-${resolutionModifier}(\\\\d{1,2})(?!\\\\S)`);\n    const colSize = parseInt(columnEls[0].className.match(colRegex)?.[1] || 12);\n    const offsetSize = getFirstItem(columnEls, isMobile).classList.contains(\n        `offset-${resolutionModifier}1`\n    )\n        ? 1\n        : 0;\n\n    return Math.floor((12 - offsetSize) / colSize);\n}\nexport const getRow = (el) => el.querySelector(\":scope > .row\");\n/**\n * Gets the first item, whether it has a mobile order or not.\n *\n * @private\n * @param {HTMLCollection} columnEls - elements in the .row container\n * @param {boolean} isMobile\n * @returns {HTMLElement} first HTMLElement in order\n */\nexport function getFirstItem(columnEls, isMobile) {\n    return (isMobile && [...columnEls].find((el) => el.style.order === \"0\")) || columnEls[0];\n}\n/**\n * Adds mobile order and the reset class for large screens.\n *\n * @private\n * @param {HTMLCollection} columnEls - elements in the .row container\n * @param {String} mobileBreakpoint - bootstrap breakpoint (sm - md - lg)\n */\nexport function addMobileOrders(columnEls, mobileBreakpoint) {\n    for (let i = 0; i < columnEls.length; i++) {\n        columnEls[i].style.order = i;\n        columnEls[i].classList.add(`order-${mobileBreakpoint}-0`);\n    }\n}\n/**\n * Removes mobile orders and the reset class for large screens.\n *\n * @private\n * @param {HTMLCollection} columnEls - elements in the .row container\n * @param {String} mobileBreakpoint - bootstrap breakpoint (sm - md - lg)\n */\nexport function removeMobileOrders(columnEls, mobileBreakpoint) {\n    for (const el of columnEls) {\n        el.style.order = \"\";\n        el.classList.remove(`order-${mobileBreakpoint}-0`);\n    }\n}\n/**\n * Checks whether some columns were resized or were added offsets manually.\n *\n * @private\n * @param {HTMLElement} columnEls\n * @param {boolean} isMobile\n * @param {String} mobileBreakpoint - bootstrap breakpoint (sm - md - lg)\n * @returns {boolean}\n */\nexport function areColsCustomized(columnEls, isMobile, mobileBreakpoint) {\n    if (!columnEls?.length) {\n        return false;\n    }\n    const resolutionModifier = isMobile ? \"\" : `${mobileBreakpoint}-`;\n    const colRegex = new RegExp(`(?:^|\\\\s+)col-${resolutionModifier}(\\\\d{1,2})(?!\\\\S)`);\n    const colSize = parseInt(columnEls[0].className.match(colRegex)?.[1] || 12);\n\n    // Cases where we know the columns sizes and/or offsets are NOT custom:\n    // - if all columns have an equal size AND\n    //     - if there are no offsets OR\n    //     - if, with 5 columns, there is exactly one offset-1 and it's on\n    //       the 1st item\n    // Any other case is custom.\n    const allColsSizesEqual = [...columnEls].every(\n        (columnEl) => parseInt(columnEl.className.match(colRegex)?.[1] || 12) === colSize\n    );\n    if (!allColsSizesEqual) {\n        return true;\n    }\n    const offsetRegex = new RegExp(`(?:^|\\\\s+)offset-${resolutionModifier}[1-9][0-1]?(?!\\\\S)`);\n    const nbOffsets = [...columnEls].filter((columnEl) =>\n        columnEl.className.match(offsetRegex)\n    ).length;\n    if (nbOffsets === 0) {\n        return false;\n    }\n    if (\n        nbOffsets === 1 &&\n        colSize === 2 &&\n        getFirstItem(columnEls, isMobile).className.match(`offset-${resolutionModifier}1`)\n    ) {\n        return false;\n    }\n    return true;\n}\n/**\n * Fill in the gap left by a removed item having a mobile order class.\n *\n * @param {HTMLElement} parentEl the removed item parent\n * @param {Number} itemOrder the removed item mobile order\n */\nexport function fillRemovedItemGap(parentEl, itemOrder) {\n    [...parentEl.children].forEach((el) => {\n        const elOrder = parseInt(el.style.order);\n        if (elOrder > itemOrder) {\n            el.style.order = elOrder - 1;\n        }\n    });\n}\n", "export function escapeTextNodes(el) {\n    const nodeFilter = (node) => {\n        if (\n            node.nodeType === Node.ELEMENT_NODE &&\n            (node.matches(\"object,iframe,script,style\") ||\n                (node.hasAttribute(\"data-oe-model\") &&\n                    node.getAttribute(\"data-oe-model\") !== \"ir.ui.view\"))\n        ) {\n            return NodeFilter.FILTER_REJECT; // Skip this node and its descendants\n        }\n        if (node.nodeType === Node.TEXT_NODE) {\n            return NodeFilter.FILTER_ACCEPT;\n        }\n        return NodeFilter.FILTER_SKIP; // Skip other nodes, but visit their children\n    };\n    if (nodeFilter(el) === NodeFilter.FILTER_REJECT) {\n        return;\n    }\n    const walker = document.createTreeWalker(el, NodeFilter.SHOW_ALL, nodeFilter);\n    const escaper = document.createElement(\"div\");\n    let node;\n    while ((node = walker.nextNode())) {\n        escaper.textContent = node.nodeValue;\n        node.nodeValue = escaper.innerHTML;\n    }\n}\n", "import { renderToElement } from \"@web/core/utils/render\";\n\nexport const rowSize = 50; // 50px.\n// Maximum number of rows that can be added when dragging a grid item.\nexport const additionalRowLimit = 10;\nconst defaultGridPadding = 10; // 10px (see `--grid-item-padding-(x|y)` CSS variables).\n\n/**\n * Returns the grid properties: rowGap, rowSize, columnGap and columnSize.\n *\n * @private\n * @param {HTMLElement} rowEl the grid element\n * @returns {Object}\n */\nexport function getGridProperties(rowEl) {\n    const style = window.getComputedStyle(rowEl);\n    const rowGap = parseFloat(style.rowGap);\n    const columnGap = parseFloat(style.columnGap);\n    const columnSize = (rowEl.clientWidth - 11 * columnGap) / 12;\n    return { rowGap, rowSize, columnGap, columnSize };\n}\n/**\n * Returns the grid item properties: row|column-start|end, grid-area and z-index\n * style properties.\n *\n * @private\n * @param {HTMLElement} gridItemEl the grid item\n * @returns {Object}\n */\nexport function getGridItemProperties(gridItemEl) {\n    const style = gridItemEl.style;\n    const rowStart = parseInt(style.gridRowStart);\n    const rowEnd = parseInt(style.gridRowEnd);\n    const columnStart = parseInt(style.gridColumnStart);\n    const columnEnd = parseInt(style.gridColumnEnd);\n\n    const gridArea = style.gridArea;\n    const zIndex = style.zIndex;\n    return { rowStart, rowEnd, columnStart, columnEnd, gridArea, zIndex };\n}\n/**\n * Sets the z-index property of the element to the maximum z-index present in\n * the grid increased by one (so it is in front of all the other elements).\n *\n * @private\n * @param {Element} element the element of which we want to set the z-index\n * @param {Element} rowEl the parent grid element of the element\n */\nexport function setElementToMaxZindex(element, rowEl) {\n    const childrenEls = [...rowEl.children].filter(\n        (el) => el !== element && !el.classList.contains(\"o_we_grid_preview\")\n    );\n    element.style.zIndex = Math.max(...childrenEls.map((el) => el.style.zIndex)) + 1;\n}\n/**\n * Creates the background grid appearing everytime a change occurs in a grid.\n *\n * @private\n * @param {Element} rowEl\n * @param {Number} gridHeight\n */\nexport function addBackgroundGrid(rowEl, gridHeight) {\n    const gridProp = getGridProperties(rowEl);\n    const rowCount = Math.max(rowEl.dataset.rowCount, gridHeight);\n\n    const backgroundGrid = renderToElement(\"html_builder.background_grid\", {\n        rowCount: rowCount + 1,\n        rowGap: gridProp.rowGap,\n        rowSize: gridProp.rowSize,\n        columnGap: gridProp.columnGap,\n        columnSize: gridProp.columnSize,\n    });\n    rowEl.prepend(backgroundGrid);\n    return rowEl.firstElementChild;\n}\n/**\n * Updates the number of rows in the grid to the end of the lowest column\n * present in it.\n *\n * @private\n * @param {Element} rowEl\n */\nexport function resizeGrid(rowEl) {\n    const columnEls = [...rowEl.children].filter((c) => c.classList.contains(\"o_grid_item\"));\n    rowEl.dataset.rowCount = Math.max(...columnEls.map((el) => el.style.gridRowEnd)) - 1;\n}\n/**\n * Removes the properties and elements added to make the drag over a grid work.\n *\n * @private\n * @param {HTMLElement} rowEl\n * @param {HTMLElement} columnEl\n * @param {HTMLElement} dragHelperEl\n * @param {HTMLElement} backgroundGridEl\n */\nexport function cleanUpGrid(rowEl, columnEl, dragHelperEl, backgroundGridEl) {\n    const columnStyleProps = [\"position\", \"top\", \"right\", \"left\", \"height\", \"width\"];\n    columnStyleProps.forEach((prop) => columnEl.style.removeProperty(prop));\n    rowEl.style.removeProperty(\"position\");\n    dragHelperEl.remove();\n    backgroundGridEl.remove();\n}\n/**\n * Toggles the row (= child element of containerEl) in grid mode.\n *\n * @private\n * @param {Element} containerEl element with the class \"container\"\n * @param {Function} preserveSelection called to preserve the text selection\n *   when needed\n * @param {String} mobileBreakpoint - bootstrap breakpoint (sm - md - lg)\n */\nexport function toggleGridMode(containerEl, preserveSelection, mobileBreakpoint) {\n    let rowEl = containerEl.querySelector(\":scope > .row\");\n    const outOfRowEls = [...containerEl.children].filter((el) => !el.classList.contains(\"row\"));\n\n    // Keep the text selection.\n    const restoreSelection =\n        !rowEl || outOfRowEls.length > 0 ? preserveSelection().restore : () => {};\n\n    // For the snippets having elements outside of the row (and therefore not in\n    // a column), create a column and put these elements in it so they can also\n    // be placed in the grid.\n    if (rowEl && outOfRowEls.length > 0) {\n        const columnEl = document.createElement(\"div\");\n        columnEl.classList.add(`col-${mobileBreakpoint}-12`);\n        for (let i = outOfRowEls.length - 1; i >= 0; i--) {\n            columnEl.prepend(outOfRowEls[i]);\n        }\n        rowEl.prepend(columnEl);\n    }\n\n    // If the number of columns is \"None\", create a column with the content.\n    if (!rowEl) {\n        rowEl = document.createElement(\"div\");\n        rowEl.classList.add(\"row\");\n\n        const columnEl = document.createElement(\"div\");\n        columnEl.classList.add(`col-${mobileBreakpoint}-12`);\n\n        const containerChildren = containerEl.children;\n        // Looping backwards because elements are removed, so the indexes are\n        // not lost.\n        for (let i = containerChildren.length - 1; i >= 0; i--) {\n            columnEl.prepend(containerChildren[i]);\n        }\n        rowEl.appendChild(columnEl);\n        containerEl.appendChild(rowEl);\n    }\n    restoreSelection();\n\n    // Converting the columns to grid and getting back the number of rows.\n    const columnEls = rowEl.children;\n    const columnSize = rowEl.clientWidth / 12;\n    rowEl.style.position = \"relative\";\n    const rowCount = placeColumns(columnEls, rowSize, 0, columnSize, 0, mobileBreakpoint) - 1;\n    rowEl.style.removeProperty(\"position\");\n    rowEl.dataset.rowCount = rowCount;\n\n    // Removing the classes that break the grid.\n    const classesToRemove = [...rowEl.classList].filter((c) => /^align-items/.test(c));\n    rowEl.classList.remove(...classesToRemove);\n\n    rowEl.classList.add(\"o_grid_mode\");\n}\n/**\n * Places each column in the grid based on their position and returns the\n * lowest row end.\n *\n * @private\n * @param {HTMLCollection} columnEls\n *      The children of the row element we are toggling in grid mode.\n * @param {Number} rowSize\n * @param {Number} rowGap\n * @param {Number} columnSize\n * @param {Number} columnGap\n * @param {String} mobileBreakpoint - bootstrap breakpoint (sm - md - lg)\n * @returns {Number}\n */\nfunction placeColumns(columnEls, rowSize, rowGap, columnSize, columnGap, mobileBreakpoint) {\n    let maxRowEnd = 0;\n    const columnSpans = [];\n    let zIndex = 1;\n    const imageColumns = []; // array of boolean telling if it is a column with only an image.\n    const isRtl = !!columnEls[0]?.closest(\".o_rtl, [dir='rtl']\");\n\n    for (const columnEl of columnEls) {\n        // Finding out if the images are alone in their column.\n        const isImageColumn = checkIfImageColumn(columnEl);\n        const imageEl = columnEl.querySelector(\"img\");\n        // Checking if the column has a background color to take that into\n        // account when computing its size and padding (to make it look good).\n        const hasBackgroundColor = columnEl.classList.contains(\"o_cc\");\n        const isImageWithoutPadding = isImageColumn && !hasBackgroundColor;\n\n        // Placing the column.\n        const style = window.getComputedStyle(columnEl);\n        // Horizontal placement.\n        const borderLeft = parseFloat(style.borderLeft);\n        let columnLeft =\n            isImageWithoutPadding && !borderLeft ? imageEl.offsetLeft : columnEl.offsetLeft;\n        if (isRtl) {\n            const parentWidth = columnEl.offsetParent.clientWidth;\n            columnLeft =\n                isImageWithoutPadding && !borderLeft\n                    ? parentWidth - imageEl.offsetLeft - imageEl.offsetWidth\n                    : parentWidth - columnEl.offsetLeft - columnEl.offsetWidth;\n        }\n        // Getting the width of the column.\n        const paddingLeft = parseFloat(style.paddingLeft);\n        let width = isImageWithoutPadding\n            ? parseFloat(imageEl.scrollWidth)\n            : parseFloat(columnEl.scrollWidth) - (hasBackgroundColor ? 0 : 2 * paddingLeft);\n        const borderX = borderLeft + parseFloat(style.borderRight);\n        width += borderX + (hasBackgroundColor || isImageColumn ? 0 : 2 * defaultGridPadding);\n        let columnSpan = Math.round((width + columnGap) / (columnSize + columnGap));\n        if (columnSpan < 1) {\n            columnSpan = 1;\n        }\n        const columnStart = Math.round(columnLeft / (columnSize + columnGap)) + 1;\n        const columnEnd = columnStart + columnSpan;\n\n        // Vertical placement.\n        const borderTop = parseFloat(style.borderTop);\n        const columnTop =\n            isImageWithoutPadding && !borderTop ? imageEl.offsetTop : columnEl.offsetTop;\n        // Getting the top and bottom paddings and computing the row offset.\n        const paddingTop = parseFloat(style.paddingTop);\n        const paddingBottom = parseFloat(style.paddingBottom);\n        const rowOffsetTop = Math.floor((paddingTop + rowGap) / (rowSize + rowGap));\n        // Getting the height of the column.\n        let height = isImageWithoutPadding\n            ? parseFloat(imageEl.scrollHeight)\n            : parseFloat(columnEl.scrollHeight) -\n              (hasBackgroundColor ? 0 : paddingTop + paddingBottom);\n        const borderY = borderTop + parseFloat(style.borderBottom);\n        height += borderY + (hasBackgroundColor || isImageColumn ? 0 : 2 * defaultGridPadding);\n        const rowSpan = Math.ceil((height + rowGap) / (rowSize + rowGap));\n        const rowStart =\n            Math.round(columnTop / (rowSize + rowGap)) +\n            1 +\n            (hasBackgroundColor || isImageWithoutPadding ? 0 : rowOffsetTop);\n        const rowEnd = rowStart + rowSpan;\n\n        columnEl.style.gridArea = `${rowStart} / ${columnStart} / ${rowEnd} / ${columnEnd}`;\n        columnEl.classList.add(\"o_grid_item\");\n\n        // Adding the grid classes.\n        columnEl.classList.add(`g-col-${mobileBreakpoint}-${columnSpan}`, `g-height-${rowSpan}`);\n        // Setting the initial z-index.\n        columnEl.style.zIndex = zIndex++;\n        // Setting the paddings.\n        if (hasBackgroundColor) {\n            columnEl.style.setProperty(\"--grid-item-padding-y\", `${paddingTop}px`);\n            columnEl.style.setProperty(\"--grid-item-padding-x\", `${paddingLeft}px`);\n        }\n        // Reload the images.\n        reloadLazyImages(columnEl);\n\n        maxRowEnd = Math.max(rowEnd, maxRowEnd);\n        columnSpans.push(columnSpan);\n        imageColumns.push(isImageColumn);\n    }\n\n    for (const [i, columnEl] of [...columnEls].entries()) {\n        // Removing padding and offset classes.\n        const regex = new RegExp(\n            `^(((pt|pb)\\\\d{1,3}$)|col-${mobileBreakpoint}-|offset-${mobileBreakpoint}-)`\n        );\n        const toRemove = [...columnEl.classList].filter((c) => regex.test(c));\n        columnEl.classList.remove(...toRemove);\n        columnEl.classList.add(`col-${mobileBreakpoint}-` + columnSpans[i]);\n\n        // If the column only has an image, convert it.\n        if (imageColumns[i]) {\n            convertImageColumn(columnEl);\n        }\n    }\n\n    return maxRowEnd;\n}\n/**\n * Removes and sets back the 'src' attribute of the images inside a column.\n * (To avoid the disappearing image problem in Chrome).\n *\n * @private\n * @param {Element} columnEl\n */\nexport function reloadLazyImages(columnEl) {\n    const imageEls = columnEl.querySelectorAll(\"img\");\n    for (const imageEl of imageEls) {\n        const src = imageEl.getAttribute(\"src\");\n        imageEl.src = \"\";\n        imageEl.src = src;\n    }\n}\n/**\n * Computes the column and row spans of the column thanks to its width and\n * height and returns them. Also adds the grid classes to the column.\n *\n * @private\n * @param {HTMLElement} rowEl\n * @param {HTMLElement} columnEl\n * @param {Number} columnWidth the width in pixels of the column\n * @param {Number} columnHeight the height in pixels of the column\n * @param {String} mobileBreakpoint - bootstrap breakpoint (sm - md - lg)\n * @returns {Object}\n */\nexport function convertColumnToGrid(rowEl, columnEl, columnWidth, columnHeight, mobileBreakpoint) {\n    // First, checking if the column only contains an image and if it is the\n    // case, converting it.\n    if (checkIfImageColumn(columnEl)) {\n        convertImageColumn(columnEl);\n    }\n\n    // Taking the grid padding into account.\n    const paddingX =\n        parseFloat(rowEl.style.getPropertyValue(\"--grid-item-padding-x\")) || defaultGridPadding;\n    const paddingY =\n        parseFloat(rowEl.style.getPropertyValue(\"--grid-item-padding-y\")) || defaultGridPadding;\n    columnWidth += 2 * paddingX;\n    columnHeight += 2 * paddingY;\n\n    // Computing the column and row spans.\n    const { rowGap, rowSize, columnGap, columnSize } = getGridProperties(rowEl);\n    const columnSpan = Math.round((columnWidth + columnGap) / (columnSize + columnGap));\n    const rowSpan = Math.ceil((columnHeight + rowGap) / (rowSize + rowGap));\n\n    // Removing the padding and offset classes.\n    const regex = /^(pt|pb|col-|offset-)/;\n    const toRemove = [...columnEl.classList].filter((c) => regex.test(c));\n    columnEl.classList.remove(...toRemove);\n\n    // Adding the grid classes.\n    columnEl.classList.add(\n        `g-col-${mobileBreakpoint}-${columnSpan}`,\n        `g-height-${rowSpan}`,\n        `col-${mobileBreakpoint}-${columnSpan}`\n    );\n    columnEl.classList.add(\"o_grid_item\");\n\n    return { columnSpan, rowSpan };\n}\n/**\n * Removes the grid properties from the grid column when it becomes a normal\n * column.\n *\n * @param {Element} columnEl\n * @param {String} mobileBreakpoint - bootstrap breakpoint (sm - md - lg)\n */\nexport function convertToNormalColumn(columnEl, mobileBreakpoint) {\n    const gridSizeClasses = columnEl.className.match(\n        new RegExp(`(g-col-${mobileBreakpoint}|g-height)-[0-9]+`, \"g\")\n    );\n    columnEl.classList.remove(\n        \"o_grid_item\",\n        \"o_grid_item_image\",\n        \"o_grid_item_image_contain\",\n        ...gridSizeClasses\n    );\n    columnEl.style.removeProperty(\"z-index\");\n    columnEl.style.removeProperty(\"--grid-item-padding-x\");\n    columnEl.style.removeProperty(\"--grid-item-padding-y\");\n    columnEl.style.removeProperty(\"grid-area\");\n}\n/**\n * Checks whether the column only contains an image or not. An image is\n * considered alone if the column only contains empty textnodes and line breaks\n * in addition to the image. Note that \"image\" also refers to an image link\n * (i.e. `a > img`).\n *\n * @private\n * @param {Element} columnEl\n * @returns {Boolean}\n */\nexport function checkIfImageColumn(columnEl) {\n    let isImageColumn = false;\n    const imageEls = columnEl.querySelectorAll(\":scope > img, :scope > a > img\");\n    const columnChildrenEls = [...columnEl.children].filter((el) => el.nodeName !== \"BR\");\n    if (imageEls.length === 1 && columnChildrenEls.length === 1) {\n        // If there is only one image and if this image is the only \"real\"\n        // child of the column, we need to check if there is text in it.\n        const textNodeEls = [...columnEl.childNodes].filter((el) => el.nodeType === Node.TEXT_NODE);\n        const areTextNodesEmpty = [...textNodeEls].every(\n            (textNodeEl) => textNodeEl.nodeValue.trim() === \"\"\n        );\n        isImageColumn = areTextNodesEmpty;\n    }\n    return isImageColumn;\n}\n/**\n * Removes the line breaks and textnodes of the column, adds the grid class and\n * sets the image width to default so it can be displayed as expected.\n *\n * @private\n * @param {Element} columnEl a column containing only an image.\n */\nfunction convertImageColumn(columnEl) {\n    columnEl.querySelectorAll(\"br\").forEach((el) => el.remove());\n    const textNodeEls = [...columnEl.childNodes].filter((el) => el.nodeType === Node.TEXT_NODE);\n    textNodeEls.forEach((el) => el.remove());\n    const imageEl = columnEl.querySelector(\"img\");\n    columnEl.classList.add(\"o_grid_item_image\");\n    imageEl.style.removeProperty(\"width\");\n}\n", "// Gives names to options sequence.\n// Module-specific sequences are defined in other option_sequence.js files.\n\nconst BEGIN = 1;\nconst END = 100;\n\n/** Ordered set of known positions. */\nconst ALL = [BEGIN, END];\n/**\n * This position should be used for non-snippet options.\n * For the default position of snippet specific options, use {@link SNIPPET_SPECIFIC}.\n */\nexport const DEFAULT = track(10);\n\n/**\n * Keeps track of a position in the ordered list positions ALL.\n *\n * @param {Number} position\n * @return {Number} position parameter itself\n */\nfunction track(position) {\n    if (!(position in ALL)) {\n        const index = ALL.findIndex((value) => value > position);\n        if (index === -1) {\n            ALL.push(position);\n        } else {\n            ALL.splice(index, 0, position);\n        }\n    }\n    return position;\n}\n/**\n * Generates 'count' positions evenly-spread between a beginPosition and an\n * endPosition.\n *\n * @param {Number} beginPosition position after which to generate positions\n * @param {Number} endPosition position before which to generate positions\n * @param {int} count amount of generated positions\n * @return {Number[]} containing {@link count} positions generated within range\n */\nexport function splitBetween(beginPosition, endPosition, count) {\n    const result = [];\n    const delta = (endPosition - beginPosition) / (count + 1);\n    for (let index = 1; index <= count; index++) {\n        result.push(track(beginPosition + delta * index));\n    }\n    return result;\n}\n/**\n * Generates a position halfway between two positions.\n *\n * @param {Number} previousPosition position after which to generate position\n * @param {Number} nextPosition position before which to generate position\n * @return {Number} position halfway between begin and end\n */\nexport function between(previousPosition, nextPosition) {\n    return splitBetween(previousPosition, nextPosition, 1)[0];\n}\n/**\n * Generates a position after the specified position, but before the next\n * already known position.\n *\n * @param {Number} position position after which to generate position\n * @return {Number} generated position\n */\nexport function after(position) {\n    const index = ALL.findIndex((value) => value === position);\n    if (index === -1) {\n        throw new Error(\"Position \" + position + \" does not exist. Do not use arbitrary numbers.\");\n    }\n    if (index === ALL.length - 1) {\n        throw new Error(\"Cannot place something after END position.\");\n    }\n    const nextPosition = ALL[index + 1];\n    return between(position, nextPosition);\n}\n/**\n * Generates a position before the specified position, but after the previous\n * already known position.\n *\n * @param {Number} position position before which to generate position\n * @return {Number} generated position\n */\nexport function before(position) {\n    const index = ALL.findIndex((value) => value === position);\n    if (index === -1) {\n        throw new Error(\"Position \" + position + \" does not exist. Do not use arbitrary numbers.\");\n    }\n    if (index === 0) {\n        throw new Error(\"Cannot place something before BEGIN position.\");\n    }\n    const previousPosition = ALL[index - 1];\n    return between(previousPosition, position);\n}\n\nconst SNIPPET_SPECIFIC = DEFAULT;\nconst [\n    REPLACE_MEDIA,\n    FONT_AWESOME,\n    IMAGE_TOOL,\n    ALIGNMENT_STYLE_PADDING,\n    DYNAMIC_SVG,\n    AFTER_HTML_BUILDER,\n    SNIPPET_SPECIFIC_BEFORE,\n    ...__DETECT_ERROR_1__\n] = splitBetween(BEGIN, SNIPPET_SPECIFIC, 7);\nif (__DETECT_ERROR_1__.length > 0) {\n    console.error(\"Wrong count in split before default\");\n}\n\nconst [\n    SNIPPET_SPECIFIC_AFTER,\n    LAYOUT_COLUMN,\n    VERTICAL_ALIGNMENT,\n    SNIPPET_SPECIFIC_NEXT,\n    SNIPPET_SPECIFIC_END,\n    ANIMATE,\n    ...__DETECT_ERROR_2__\n] = splitBetween(SNIPPET_SPECIFIC, END, 6);\nif (__DETECT_ERROR_2__.length > 0) {\n    console.error(\"Wrong count in split after default\");\n}\n\nconst [TEXT_ALIGNMENT, TITLE_LAYOUT_SIZE, WIDTH, BLOCK_ALIGN, ...__DETECT_ERROR_3__] = splitBetween(\n    AFTER_HTML_BUILDER,\n    SNIPPET_SPECIFIC_BEFORE,\n    4\n);\nif (__DETECT_ERROR_3__.length > 0) {\n    console.error(\"Wrong count in website split before specific\");\n}\nexport { TEXT_ALIGNMENT, TITLE_LAYOUT_SIZE, WIDTH, BLOCK_ALIGN };\n\nexport {\n    BEGIN,\n    REPLACE_MEDIA,\n    FONT_AWESOME,\n    IMAGE_TOOL,\n    ALIGNMENT_STYLE_PADDING,\n    DYNAMIC_SVG,\n    AFTER_HTML_BUILDER,\n    SNIPPET_SPECIFIC_BEFORE,\n    SNIPPET_SPECIFIC,\n    SNIPPET_SPECIFIC_AFTER,\n    LAYOUT_COLUMN,\n    VERTICAL_ALIGNMENT,\n    SNIPPET_SPECIFIC_NEXT,\n    SNIPPET_SPECIFIC_END,\n    ANIMATE,\n    END,\n};\n", "// Scrolling util functions needed by the frontend apps and sub-modules. These\n// functions indeed take into account all frontend-specific concepts (like the\n// header at the top of the page, the wrapwrap,...) which are not considered in\n// the `@web/core/utils/scrolling` utils.\n\nimport { getScrollingElement } from \"@web/core/utils/scrolling\";\n\n/**\n * Determines if an element is scrollable.\n *\n * @param {Element} element - the element to check\n * @returns {Boolean}\n */\nfunction isScrollable(element) {\n    if (!element) {\n        return false;\n    }\n    const overflowY = window.getComputedStyle(element).overflowY;\n    return (\n        overflowY === \"auto\" ||\n        overflowY === \"scroll\" ||\n        (overflowY === \"visible\" && element === element.ownerDocument.scrollingElement)\n    );\n}\n\n/**\n * Finds the closest scrollable element for the given element.\n *\n * @param {Element} element - The element to find the closest scrollable element for.\n * @returns {Element} The closest scrollable element.\n */\nexport function closestScrollable(element) {\n    const document = element.ownerDocument || window.document;\n\n    while (element && element !== document.scrollingElement) {\n        if (element instanceof Document) {\n            return null;\n        }\n        if (isScrollable(element)) {\n            return element;\n        }\n        element = element.parentElement;\n    }\n    return element || document.scrollingElement;\n}\n\n/**\n * Computes the size by which a scrolling point should be decreased so that\n * the top fixed elements of the page appear above that scrolling point.\n *\n * @param {Document} [doc=document]\n * @returns {number}\n */\nfunction scrollFixedOffset(doc = document) {\n    let size = 0;\n    const elements = doc.querySelectorAll(\".o_top_fixed_element\");\n\n    elements.forEach((el) => {\n        size += el.offsetHeight;\n    });\n\n    return size;\n}\n\n/**\n * @param {HTMLElement|string} el - the element to scroll to. If \"el\" is a\n *      string, it must be a valid selector of an element in the DOM or\n *      '#top' or '#bottom'. If it is an HTML element, it must be present\n *      in the DOM.\n *      Limitation: if the element is using a fixed position, this\n *      function cannot work except if is the header (el is then either a\n *      string set to '#top' or an HTML element with the \"top\" id) or the\n *      footer (el is then a string set to '#bottom' or an HTML element\n *      with the \"bottom\" id) for which exceptions have been made.\n * @param {number} [options] - options for the scroll behavior\n * @param {number} [options.extraOffset=0]\n *      extra offset to add on top of the automatic one (the automatic one\n *      being computed based on fixed header sizes)\n * @param {number} [options.forcedOffset]\n *      offset used instead of the automatic one (extraOffset will be\n *      ignored too)\n * @param {HTMLElement} [options.scrollable] the element to scroll\n * @param {number} [options.duration] the scroll duration in ms\n * @return {Promise}\n */\nexport function scrollTo(el, options = {}) {\n    if (!el) {\n        throw new Error(\"The scrollTo function was called without any given element\");\n    }\n    if (typeof el === \"string\") {\n        el = document.querySelector(el);\n    }\n    const isTopOrBottomHidden = el === \"top\" || el === \"bottom\";\n    const scrollable = isTopOrBottomHidden\n        ? document.scrollingElement\n        : options.scrollable || closestScrollable(el.parentElement);\n    const scrollDocument = scrollable.ownerDocument;\n    const isInOneDocument = isTopOrBottomHidden || scrollDocument === el.ownerDocument;\n    const iframe =\n        !isInOneDocument &&\n        Array.from(scrollable.querySelectorAll(\"iframe\")).find((node) =>\n            node.contentDocument.contains(el)\n        );\n    const topLevelScrollable = getScrollingElement(scrollDocument);\n\n    function _computeScrollTop() {\n        if (el === \"#top\" || el.id === \"top\") {\n            return 0;\n        }\n        if (el === \"#bottom\" || el.id === \"bottom\") {\n            return scrollable.scrollHeight - scrollable.clientHeight;\n        }\n\n        el.classList.add(\"o_check_scroll_position\");\n        let offsetTop = el.getBoundingClientRect().top + window.scrollY;\n        el.classList.remove(\"o_check_scroll_position\");\n        if (el.classList.contains(\"d-none\")) {\n            el.classList.remove(\"d-none\");\n            offsetTop = el.getBoundingClientRect().top + window.scrollY;\n            el.classList.add(\"d-none\");\n        }\n        const isDocScrollingEl = scrollable === el.ownerDocument.scrollingElement;\n        let elPosition =\n            offsetTop -\n            (scrollable.getBoundingClientRect().top +\n                window.scrollY -\n                (isDocScrollingEl ? 0 : scrollable.scrollTop));\n        if (!isInOneDocument && iframe) {\n            elPosition += iframe.getBoundingClientRect().top + window.scrollY;\n        }\n        let offset = options.forcedOffset;\n        if (offset === undefined) {\n            offset =\n                (scrollable === topLevelScrollable ? scrollFixedOffset(scrollDocument) : 0) +\n                (options.extraOffset || 0);\n        }\n        return Math.max(0, elPosition - offset);\n    }\n\n    return new Promise((resolve) => {\n        const start = scrollable.scrollTop;\n        const duration = options.duration || 600;\n        const startTime = performance.now();\n\n        function animateScroll(currentTime) {\n            const elapsedTime = currentTime - startTime;\n            const progress = Math.min(elapsedTime / duration, 1);\n            const easeInOutQuad =\n                progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;\n            // Recompute the scroll destination every time, to adapt to any\n            // occurring change that would modify the scroll offset.\n            const change = _computeScrollTop() - start;\n            const newScrollTop = start + change * easeInOutQuad;\n\n            scrollable.scrollTop = newScrollTop;\n\n            if (elapsedTime < duration) {\n                requestAnimationFrame(animateScroll);\n            } else {\n                resolve();\n            }\n        }\n\n        requestAnimationFrame(animateScroll);\n    });\n}\n", "import { Cache } from \"@web/core/utils/cache\";\n\nexport class SyncCache {\n    constructor(fn) {\n        this.asyncCache = new Cache(fn, JSON.stringify);\n        this.syncCache = new Map();\n    }\n    async preload(params) {\n        const result = await this.asyncCache.read(params);\n        this.syncCache.set(JSON.stringify(params), result);\n        return result;\n    }\n    get(params) {\n        return this.syncCache.get(JSON.stringify(params));\n    }\n    invalidate() {\n        this.asyncCache.invalidate();\n        this.syncCache.clear();\n    }\n}\n", "import { DependencyManager } from \"../core/dependency_manager\";\nimport { useSubEnv } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * Retrieves the default name corresponding to the edited element (to display it\n * in the sidebar for example).\n *\n * @param {HTMLElement} snippetEl - the edited element\n * @returns {String}\n */\nexport function getSnippetName(snippetEl) {\n    if (snippetEl.dataset.name) {\n        return snippetEl.dataset.name;\n    }\n    if (snippetEl.matches(\"img\")) {\n        return _t(\"Image\");\n    }\n    if (snippetEl.matches(\".fa\")) {\n        return _t(\"Icon\");\n    }\n    if (snippetEl.matches(\".media_iframe_video\")) {\n        return _t(\"Video\");\n    }\n    if (snippetEl.parentNode?.matches(\".row\")) {\n        return _t(\"Column\");\n    }\n    if (snippetEl.matches(\"#wrapwrap > main\")) {\n        return _t(\"Page Options\");\n    }\n    if (snippetEl.matches(\".btn\")) {\n        return _t(\"Button\");\n    }\n    return _t(\"Block\");\n}\n\n/**\n * Checks if the element is visible (= in the viewport).\n *\n * @param {HTMLElement} el\n * @returns {Boolean}\n */\nexport function isElementInViewport(el) {\n    const rect = el.getBoundingClientRect();\n    const viewportWidth = window.innerWidth || document.documentElement.clientWidth;\n    const viewportHeight = window.innerHeight || document.documentElement.clientHeight;\n    return (\n        Math.round(rect.top) >= 0 &&\n        Math.round(rect.left) >= 0 &&\n        Math.round(rect.right) <= viewportWidth &&\n        Math.round(rect.bottom) <= viewportHeight\n    );\n}\n\n/**\n * Checks if the given element is visible in the sense of the jQuery `:visible`\n * selector.\n *\n * @param {HTMLElement} el the element\n * @returns {Boolean}\n */\nexport function isVisible(el) {\n    if (el.offsetHeight > 0 || el.offsetWidth > 0) {\n        return true;\n    }\n    return false;\n}\n\n/**\n * Gets all the elements matching an option selector/exclude starting from the\n * root element.\n *\n * @param {HTMLElement} rootEl\n * @param {String} selector\n * @param {String} exclude\n * @param {String} applyTo\n * @returns {Array}\n */\nexport function getElementsWithOption(rootEl, selector, exclude = false, applyTo = false) {\n    let matchingEls = [...rootEl.querySelectorAll(selector)];\n    if (rootEl.matches(selector)) {\n        matchingEls.unshift(rootEl);\n    }\n    if (exclude) {\n        matchingEls = matchingEls.filter((editingEl) => !editingEl.matches(exclude));\n    }\n    if (applyTo) {\n        matchingEls = matchingEls.flatMap((editingEl) => [...editingEl.querySelectorAll(applyTo)]);\n    }\n    return matchingEls;\n}\n\nexport function useOptionsSubEnv(getEditingElements) {\n    useSubEnv({\n        dependencyManager: new DependencyManager(),\n        getEditingElement: () => getEditingElements()[0],\n        getEditingElements: getEditingElements,\n        weContext: {},\n    });\n}\n\nexport function getValueFromVar(value) {\n    const match = value.match(/var\\(--([a-zA-Z0-9-_]+)\\)/);\n    if (match) {\n        return match[1];\n    }\n    return value;\n}\n\n/**\n * Converts a value to a ratio.\n *\n * @param {string} value\n */\nexport function toRatio(value) {\n    const inputValueAsNumber = Number(value);\n    const ratio = inputValueAsNumber >= 0 ? 1 + inputValueAsNumber : 1 / (1 - inputValueAsNumber);\n    return `${ratio.toFixed(2)}x`;\n}\n\n/**\n * Filters an array of classes to only include those that extend a given class.\n */\nexport function filterExtends(arr, PotentialSuperClass) {\n    return arr.filter((PotentialSubClass) =>\n        doesExtendsClass(PotentialSubClass, PotentialSuperClass)\n    );\n}\n\n/**\n * Checks if a `potentialSubClass` directly or indirectly extends a\n * `potentialSuperClass`.\n *\n * The implementation leverages the fact that classes are functions and their\n * prototype chain reflects the inheritance.\n *\n * @param {Function} PotentialSubClass The class that might be a subclass.\n * @param {Function} PotentialSuperClass The class that might be a superclass.\n * @returns {boolean} True if `potentialSubClass` extends `potentialSuperClass`,\n * false otherwise.\n */\nexport function doesExtendsClass(PotentialSubClass, PotentialSuperClass) {\n    if (PotentialSubClass === PotentialSuperClass) {\n        return false;\n    }\n    return PotentialSubClass.prototype instanceof PotentialSuperClass;\n}\n\n/**\n * Checks if the given element is editable.\n *\n * @param {HTMLElement} node the element\n * @returns {Boolean}\n */\nexport function isEditable(node) {\n    let currentNode = node;\n    while (currentNode) {\n        if (currentNode.className && typeof currentNode.className === \"string\") {\n            if (currentNode.className.includes(\"o_not_editable\")) {\n                return false;\n            }\n            if (currentNode.className.includes(\"o_editable\")) {\n                return true;\n            }\n        }\n        currentNode = currentNode.parentNode;\n    }\n    return false;\n}\n\n/**\n * Removes the specified plugins from a given list of plugins.\n *\n * @param {Array<Plugin>} plugins the list of plugins\n * @param {Array<string>} pluginsToRemove the names of the plugins to remove\n * @returns {Array<Plugin>}\n */\nexport function removePlugins(plugins, pluginsToRemove) {\n    return plugins.filter((p) => !pluginsToRemove.includes(p.name));\n}\n\n/**\n * Check if the given value is an integer smaller than 15 digits.\n * @param {String} value\n * @returns {Boolean}\n */\nexport function isSmallInteger(value) {\n    return /^-?[0-9]{1,15}$/.test(value);\n}\n", "import { EDITOR_COLOR_CSS_VARIABLES, isColorCombinationName } from \"@html_editor/utils/color\";\nimport { selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { backgroundImageCssToParts, getBgImageURLFromURL } from \"@html_editor/utils/image\";\nimport { normalizeCSSColor, isCSSColor, isColorGradient, rgbaToHex } from \"@web/core/utils/colors\";\nimport { convertNumericToUnit, getCSSVariableValue } from \"@html_editor/utils/formatting\";\n\n/**\n * window.getComputedStyle cannot work properly with CSS shortcuts (like\n * 'border-width' which is a shortcut for the top + right + bottom + left border\n * widths. If an option wants to customize such a shortcut, it should be listed\n * here with the non-shortcuts property it stands for, in order.\n *\n * @type {Object<string[]>}\n */\nexport const CSS_SHORTHANDS = {\n    \"border-width\": [\n        \"border-top-width\",\n        \"border-right-width\",\n        \"border-bottom-width\",\n        \"border-left-width\",\n    ],\n    \"border-radius\": [\n        \"border-top-left-radius\",\n        \"border-top-right-radius\",\n        \"border-bottom-right-radius\",\n        \"border-bottom-left-radius\",\n    ],\n    \"--box-border-width\": [\n        \"--box-border-top-width\",\n        \"--box-border-right-width\",\n        \"--box-border-bottom-width\",\n        \"--box-border-left-width\",\n    ],\n    \"--box-border-radius\": [\n        \"--box-border-top-left-radius\",\n        \"--box-border-top-right-radius\",\n        \"--box-border-bottom-right-radius\",\n        \"--box-border-bottom-left-radius\",\n    ],\n    \"border-color\": [\n        \"border-top-color\",\n        \"border-right-color\",\n        \"border-bottom-color\",\n        \"border-left-color\",\n    ],\n    \"border-style\": [\n        \"border-top-style\",\n        \"border-right-style\",\n        \"border-bottom-style\",\n        \"border-left-style\",\n    ],\n    padding: [\"padding-top\", \"padding-right\", \"padding-bottom\", \"padding-left\"],\n};\n/**\n * Set of all the data attributes relative to the background images.\n */\nconst BACKGROUND_IMAGE_ATTRIBUTES = new Set([\n    \"originalId\",\n    \"originalSrc\",\n    \"mimetype\",\n    \"resizeWidth\",\n    \"glFilter\",\n    \"quality\",\n    \"filterOptions\",\n    \"mimetypeBeforeConversion\",\n]);\n\n/**\n * Converts the given (value + unit) string to a numeric value expressed in\n * the other given css unit.\n *\n * e.g. fct('400ms', 's') -> 0.4\n *\n * @param {string} value\n * @param {string} unitTo\n * @param {string} [cssProp] - the css property on which the unit applies\n * @returns {number}\n */\nexport function convertValueToUnit(value, unitTo, htmlStyle) {\n    const m = getNumericAndUnit(value);\n    if (!m) {\n        return NaN;\n    }\n    const numValue = parseFloat(m[0]);\n    const valueUnit = m[1];\n    return convertNumericToUnit(numValue, valueUnit, unitTo, htmlStyle);\n}\n/**\n * Returns the numeric value and unit of a css value.\n *\n * e.g. fct('400ms') -> [400, 'ms']\n *\n * @param {string} value\n * @returns {Array|null}\n */\nexport function getNumericAndUnit(value) {\n    const m = value.trim().match(/^(-?[0-9.]+(?:e[+|-]?[0-9]+)?)\\s*([^\\s]*)$/);\n    if (!m) {\n        return null;\n    }\n    return [m[1].trim(), m[2].trim()];\n}\n/**\n * Checks if two css values are equal.\n *\n * @param {string} value1\n * @param {string} value2\n * @param {string} cssProp - the css property on which the unit applies\n * @param {CSSStyleDeclaration} htmlStyle\n * @returns {boolean}\n */\nexport function areCssValuesEqual(value1, value2, cssProp, htmlStyle) {\n    // String comparison first\n    if (value1 === value2) {\n        return true;\n    }\n\n    // In case the values are a size, they might be made of two parts.\n    // TODO this seems completely arbitrary to rely on the presence of '-size'\n    // to do this: should not we just list the right CSS property explicitly\n    // if we want to do this? That would have avoided the CSS variable fix that\n    // had to be made here (the '--' part).\n    if (cssProp && cssProp.endsWith(\"-size\") && !cssProp.startsWith(\"--\")) {\n        // Avoid re-splitting each part during their individual comparison.\n        const pseudoPartProp = cssProp + \"-part\";\n        const re = /-?[0-9.]+(?:e[+|-]?[0-9]+)?\\s*[A-Za-z%-]+|auto/g;\n        const parts1 = value1.match(re);\n        const parts2 = value2.match(re);\n        for (const index of [0, 1]) {\n            const part1 = parts1 && parts1.length > index ? parts1[index] : \"auto\";\n            const part2 = parts2 && parts2.length > index ? parts2[index] : \"auto\";\n            if (!areCssValuesEqual(part1, part2, pseudoPartProp, htmlStyle)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    // It could be a CSS variable, in that case the actual value has to be\n    // retrieved before comparing.\n    if (isCSSVariable(value1)) {\n        value1 = getCSSVariableValue(value1.substring(6, value1.length - 1), htmlStyle);\n    }\n    if (isCSSVariable(value2)) {\n        value2 = getCSSVariableValue(value2.substring(6, value2.length - 1), htmlStyle);\n    }\n    if (value1 === value2) {\n        return true;\n    }\n\n    // They may be colors, normalize then re-compare the resulting string\n    const color1 = normalizeCSSColor(value1);\n    const color2 = normalizeCSSColor(value2);\n    if (color1 === color2) {\n        return true;\n    }\n\n    // They may be gradients\n    const value1IsGradient = isColorGradient(value1);\n    const value2IsGradient = isColorGradient(value2);\n    if (value1IsGradient !== value2IsGradient) {\n        return false;\n    }\n    if (value1IsGradient) {\n        // Kinda hacky and probably inneficient but probably the easiest way:\n        // applied the value as background-image of two fakes elements and\n        // compare their computed value.\n        const temp1El = document.createElement(\"div\");\n        temp1El.style.backgroundImage = value1;\n        document.body.appendChild(temp1El);\n        value1 = getComputedStyle(temp1El).backgroundImage;\n        document.body.removeChild(temp1El);\n\n        const temp2El = document.createElement(\"div\");\n        temp2El.style.backgroundImage = value2;\n        document.body.appendChild(temp2El);\n        value2 = getComputedStyle(temp2El).backgroundImage;\n        document.body.removeChild(temp2El);\n\n        return value1 === value2;\n    }\n\n    // In case the values are meant as box-shadow, this is difficult to compare.\n    // In this case we use the kinda hacky and probably inneficient but probably\n    // easiest way: applying the value as box-shadow of two fakes elements and\n    // compare their computed value.\n    if (cssProp === \"box-shadow\") {\n        const temp1El = document.createElement(\"div\");\n        temp1El.style.boxShadow = value1;\n        document.body.appendChild(temp1El);\n        value1 = getComputedStyle(temp1El).boxShadow;\n        document.body.removeChild(temp1El);\n\n        const temp2El = document.createElement(\"div\");\n        temp2El.style.boxShadow = value2;\n        document.body.appendChild(temp2El);\n        value2 = getComputedStyle(temp2El).boxShadow;\n        document.body.removeChild(temp2El);\n\n        return value1 === value2;\n    }\n\n    // Convert the second value in the unit of the first one and compare\n    // floating values\n    const data = getNumericAndUnit(value1);\n    if (!data) {\n        return false;\n    }\n    const numValue1 = data[0];\n    const numValue2 = convertValueToUnit(value2, data[1], htmlStyle);\n    return Math.abs(numValue1 - numValue2) < Number.EPSILON;\n}\n/**\n * @param {string} value\n * @returns {boolean}\n */\nexport function isCSSVariable(value) {\n    value = value.replace(/^'|'$/g, \"\");\n    return /^var\\(--.+?\\)$/.test(value);\n}\n/**\n * @param {string[]} colorNames\n * @param {string} [prefix='bg-']\n * @returns {string[]}\n */\nexport function computeColorClasses(colorNames, prefix = \"bg-\") {\n    let hasCCClasses = false;\n    const isBgPrefix = prefix === \"bg-\";\n    const classes = colorNames.map((c) => {\n        if (isBgPrefix && isColorCombinationName(c)) {\n            hasCCClasses = true;\n            return `o_cc${c}`;\n        }\n        return prefix + c;\n    });\n    if (hasCCClasses) {\n        classes.push(\"o_cc\");\n    }\n    return classes;\n}\n/**\n * Normalize a color in case it is a variable name so it can be used outside of\n * css.\n *\n * @param {string} color the color to normalize into a css value\n * @returns {string} the normalized color\n */\nexport function normalizeColor(color, htmlStyle) {\n    if (isCSSColor(color)) {\n        return color;\n    }\n    return getCSSVariableValue(color, htmlStyle);\n}\n/**\n * Parse an element's background-image's url.\n *\n * @param {HTMLElement} el\n * @returns {string|false} the src of the image or false if not parsable\n */\nexport function getBgImageURLFromEl(el) {\n    const style = el.ownerDocument.defaultView.getComputedStyle(el);\n    const parts = backgroundImageCssToParts(style.backgroundImage);\n    const string = parts.url || \"\";\n    return getBgImageURLFromURL(string);\n}\n/**\n * Generates a string ID.\n *\n * @private\n * @returns {string}\n */\nexport function generateHTMLId() {\n    return `o${Math.random().toString(36).substring(2, 15)}`;\n}\n/**\n * Returns the class of the element that matches the specified prefix.\n *\n * @private\n * @param {Element} el element from which to recover the color class\n * @param {string[]} colorNames\n * @param {string} prefix prefix of the color class to recover\n * @returns {string} color class matching the prefix or an empty string\n */\nexport function getColorClass(el, colorNames, prefix) {\n    const prefixedColorNames = computeColorClasses(colorNames, prefix);\n    return el.classList.value\n        .split(\" \")\n        .filter((cl) => prefixedColorNames.includes(cl))\n        .join(\" \");\n}\n/**\n * Add one or more new attributes related to background images in the\n * BACKGROUND_IMAGE_ATTRIBUTES set.\n *\n * @param {...string} newAttributes The new attributes to add in the\n * BACKGROUND_IMAGE_ATTRIBUTES set.\n */\nexport function addBackgroundImageAttributes(...newAttributes) {\n    BACKGROUND_IMAGE_ATTRIBUTES.add(...newAttributes);\n}\n/**\n * Check if an attribute is in the BACKGROUND_IMAGE_ATTRIBUTES set.\n *\n * @param {string} attribute The attribute that has to be checked.\n */\nexport function isBackgroundImageAttribute(attribute) {\n    return BACKGROUND_IMAGE_ATTRIBUTES.has(attribute);\n}\n/**\n * Checks if an element supposedly marked with the o_editable_media class should\n * in fact be editable (checks if its environment looks like a non editable\n * environment whose media should be editable).\n *\n * TODO: the name of this function is voluntarily bad to reflect the fact that\n * this system should be improved. The combination of o_not_editable,\n * o_editable, getContentEditableAreas, getReadOnlyAreas and other concepts\n * related to what should be editable or not should be reviewed.\n *\n * @returns {boolean}\n */\nexport function shouldEditableMediaBeEditable(mediaEl) {\n    // Some sections of the DOM are contenteditable=\"false\" (for\n    // example with the help of the o_not_editable class) but have\n    // inner media that should be editable (the fact the container\n    // is not is to prevent adding text in between those medias).\n    // This case is complex and the solution to support it is not\n    // perfect: we mark those media with a class and check that they\n    // are descendant of a savable.\n    return mediaEl.parentElement && mediaEl.parentElement.closest(\".o_editable\");\n}\n/**\n * Returns the label of a link element.\n *\n * @param {HTMLElement} linkEl\n * @returns {string}\n */\nexport function getLinkLabel(linkEl) {\n    return linkEl.textContent.replaceAll(\"\\u200B\", \"\").replaceAll(\"\\uFEFF\", \"\");\n}\n/**\n * Forwards an image source to its carousel thumbnail.\n * @param {HTMLElement} imgEl\n */\nexport function forwardToThumbnail(imgEl) {\n    const carouselEl = imgEl.closest(\".carousel\");\n    if (carouselEl) {\n        const carouselInnerEl = imgEl.closest(\".carousel-inner\");\n        const carouselItemEl = imgEl.closest(\".carousel-item\");\n        if (carouselInnerEl && carouselItemEl) {\n            const imageIndex = [...carouselInnerEl.children].indexOf(carouselItemEl);\n            const miniatureEl = carouselEl.querySelector(\n                `.carousel-indicators [data-bs-slide-to=\"${imageIndex}\"]`\n            );\n            if (miniatureEl && miniatureEl.style.backgroundImage) {\n                miniatureEl.style.backgroundImage = `url(${imgEl.getAttribute(\"src\")})`;\n            }\n        }\n    }\n}\n\n/**\n * @param {HTMLImageElement} img\n * @returns {Promise<Boolean>}\n */\nexport async function isImageCorsProtected(img) {\n    const src = img.getAttribute(\"src\");\n    if (!src) {\n        return false;\n    }\n    let isCorsProtected = false;\n    if (!src.startsWith(\"/\") || /\\/web\\/image\\/\\d+-redirect\\//.test(src)) {\n        // The `fetch()` used later in the code might fail if the image is\n        // CORS protected. We check upfront if it's the case.\n        // Two possible cases:\n        // 1. the `src` is an absolute URL from another domain.\n        //    For instance, abc.odoo.com vs abc.com which are actually the\n        //    same database behind.\n        // 2. A \"attachment-url\" which is just a redirect to the real image\n        //    which could be hosted on another website.\n        isCorsProtected = await fetch(src, { method: \"HEAD\" })\n            .then(() => false)\n            .catch(() => true);\n    }\n    return isCorsProtected;\n}\n\n/**\n * Applies only the needed CSS in the style attribute:\n * - no attribute if value is already the wanted one (possibly from a class)\n * - plain attribute if that change is sufficient to make it applied\n * - important attribute if the plain one did not work\n *\n * @param {HTMLElement} el\n * @param {string} cssProp\n * @param {string} cssValue\n * @param {CSSStyleDeclaration} computedStyle of el\n * @param {boolean} force to always apply as important\n * @param {boolean} allowImportant to avoid applying the style as important\n * @returns {boolean} if a value was applied\n */\nexport function applyNeededCss(\n    el,\n    cssProp,\n    cssValue,\n    computedStyle = el.ownerDocument.defaultView.getComputedStyle(el),\n    { force = false, allowImportant = true } = {}\n) {\n    if (force) {\n        el.style.setProperty(cssProp, cssValue, allowImportant ? \"important\" : \"\");\n        return true;\n    }\n    el.style.removeProperty(cssProp);\n    if (\n        !areCssValuesEqual(\n            computedStyle.getPropertyValue(cssProp),\n            cssValue,\n            cssProp,\n            computedStyle\n        )\n    ) {\n        el.style.setProperty(cssProp, cssValue);\n        // If change had no effect then make it important.\n        if (\n            allowImportant &&\n            !areCssValuesEqual(\n                computedStyle.getPropertyValue(cssProp),\n                cssValue,\n                cssProp,\n                computedStyle\n            )\n        ) {\n            el.style.setProperty(cssProp, cssValue, \"important\");\n        }\n        return true;\n    }\n    return false;\n}\n\nconst builderStylesheet = new CSSStyleSheet();\nexport function setBuilderCSSVariables(htmlStyle) {\n    const styles = [];\n    for (const style of EDITOR_COLOR_CSS_VARIABLES) {\n        let value = getCSSVariableValue(style, htmlStyle);\n        if (value.startsWith(\"'\") && value.endsWith(\"'\")) {\n            // Gradient values are recovered within a string.\n            value = value.substring(1, value.length - 1);\n        }\n        styles.push(`--hb-cp-${style}: ${value};`);\n    }\n    builderStylesheet.replaceSync(`html { ${styles.join(\" \")} }`);\n    if (!window.top.document.adoptedStyleSheets.find((style) => style === builderStylesheet)) {\n        window.top.document.adoptedStyleSheets.push(builderStylesheet);\n    }\n}\n\nexport function parseBoxShadow(value) {\n    const regex =\n        /(?<color>(rgb(a)?\\([^)]*\\))|(var\\([^)]+\\)))\\s+(?<offsetX>-?\\d+\\.?\\d*px)\\s+(?<offsetY>-?\\d+\\.?\\d*px)\\s+(?<blur>-?\\d+\\.?\\d*px)\\s+(?<spread>-?\\d+\\.?\\d*px)(?:\\s+(?<mode>\\w+))?/;\n    return value.match(regex).groups;\n}\n\nexport function getAllUsedColors(el) {\n    const usedCustomColors = new Set();\n    const collectColor = (colorValue) => {\n        if (isCSSColor(colorValue)) {\n            usedCustomColors.add(rgbaToHex(colorValue));\n        }\n    };\n    for (const coloredEl of selectElements(el, '[style*=\"color\"]')) {\n        for (const colorProperty of [\"color\", \"background-color\", \"border-color\"]) {\n            collectColor(coloredEl.style[colorProperty]);\n        }\n    }\n    for (const shadowEl of selectElements(el, '[style*=\"box-shadow\"]')) {\n        const shadowValue = shadowEl.style[\"box-shadow\"];\n        if (shadowValue) {\n            collectColor(parseBoxShadow(shadowValue).color);\n        }\n    }\n    // Find data-*color attributes.\n    for (const dataColoredEl of selectElements(el, \"*\")) {\n        for (const attributeName of Object.keys(dataColoredEl.dataset)) {\n            if (attributeName.endsWith(\"olor\")) {\n                collectColor(dataColoredEl.dataset[attributeName]);\n            }\n        }\n    }\n    // Shapes & illustrations.\n    const collectUrlColors = (urlString) => {\n        const url = new URL(urlString, window.location);\n        for (const colorKey of [...url.searchParams.keys()].filter((key) => /c\\d/.test(key))) {\n            collectColor(url.searchParams.get(colorKey));\n        }\n    };\n    for (const imgEl of selectElements(\n        el,\n        'img[src^=\"/html_editor/shape/\"], img[src^=\"/web_editor/shape/\"]'\n    )) {\n        collectUrlColors(imgEl.src);\n    }\n    for (const bgEl of selectElements(\n        el,\n        `[style*=\"background-image: url(\\\\\"/html_editor/shape/\"], [style*=\"background-image: url(\\\\\"/web_editor/shape/\"]`\n    )) {\n        collectUrlColors(getBgImageURLFromEl(bgEl));\n    }\n    return usedCustomColors;\n}\n", "import { useEffect } from \"@odoo/owl\";\nimport wUtils from \"@website/js/utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { BuilderUrlPicker } from \"@html_builder/core/building_blocks/builder_urlpicker\";\n\nexport class WebsiteUrlPicker extends BuilderUrlPicker {\n    setup() {\n        super.setup();\n\n        useEffect(\n            (inputEl) => {\n                if (!inputEl) {\n                    return;\n                }\n                const unmountAutocompleteWithPages = wUtils.autocompleteWithPages(\n                    inputEl,\n                    {\n                        classes: {\n                            \"ui-autocomplete\": \"o_website_ui_autocomplete\",\n                        },\n                        body: this.env.getEditingElement().ownerDocument.body,\n                        urlChosen: () => {\n                            this.commit(this.inputRef.el.value);\n                        },\n                    },\n                    this.env\n                );\n                return () => unmountAutocompleteWithPages();\n            },\n            () => [this.inputRef.el]\n        );\n    }\n}\n\nclass UrlPickerPlugin extends Plugin {\n    static id = \"urlPickerPlugin\";\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_components: {\n            WebsiteUrlPicker,\n        },\n    };\n}\n\nregistry.category(\"website-plugins\").add(UrlPickerPlugin.id, UrlPickerPlugin);\n", "import {\n    splitBetween,\n    SNIPPET_SPECIFIC_AFTER,\n    VERTICAL_ALIGNMENT,\n    LAYOUT_COLUMN,\n    SNIPPET_SPECIFIC_NEXT,\n    SNIPPET_SPECIFIC_END,\n    END,\n    ANIMATE,\n} from \"@html_builder/utils/option_sequence\";\n\n// Gives names to website options sequence.\nconst [LAYOUT, ...__DETECT_ERROR_WEBSITE_0__] = splitBetween(\n    SNIPPET_SPECIFIC_AFTER,\n    LAYOUT_COLUMN,\n    1\n);\nif (__DETECT_ERROR_WEBSITE_0__.length > 0) {\n    console.error(\"Wrong count in website split after specific\");\n}\nconst [WEBSITE_BACKGROUND_OPTIONS, BOX_BORDER_SHADOW, ...__DETECT_ERROR_WEBSITE_1__] = splitBetween(\n    VERTICAL_ALIGNMENT,\n    SNIPPET_SPECIFIC_NEXT,\n    2\n);\nif (__DETECT_ERROR_WEBSITE_1__.length > 0) {\n    console.error(\"Wrong count in website split after vertical alignment\");\n}\nconst [LAYOUT_GRID, ...__DETECT_ERROR_WEBSITE_2__] = splitBetween(\n    LAYOUT_COLUMN,\n    VERTICAL_ALIGNMENT,\n    1\n);\nif (__DETECT_ERROR_WEBSITE_2__.length > 0) {\n    console.error(\"Wrong count in website split after column layout\");\n}\nconst [GRID_COLUMNS, ...__DETECT_ERROR_WEBSITE_3__] = splitBetween(\n    VERTICAL_ALIGNMENT,\n    SNIPPET_SPECIFIC_NEXT,\n    1\n);\nif (__DETECT_ERROR_WEBSITE_3__.length > 0) {\n    console.error(\"Wrong count in website split after vertical alignment\");\n}\nconst [COVER_PROPERTIES, CONTAINER_WIDTH, SCROLL_BUTTON, ...__DETECT_ERROR_WEBSITE_4__] =\n    splitBetween(SNIPPET_SPECIFIC_NEXT, SNIPPET_SPECIFIC_END, 3);\nif (__DETECT_ERROR_WEBSITE_4__.length > 0) {\n    console.error(\"Wrong count in website split before specific end\");\n}\n\nconst [GRID_IMAGE, TEXT_HIGHLIGHT, ...__DETECT_ERROR_WEBSITE_5__] = splitBetween(\n    SNIPPET_SPECIFIC_END,\n    ANIMATE,\n    2\n);\nif (__DETECT_ERROR_WEBSITE_5__.length > 0) {\n    console.error(\"Wrong count in website split before animate\");\n}\n\nconst [CONDITIONAL_VISIBILITY, DEVICE_VISIBILITY, ...__DETECT_ERROR_WEBSITE_6__] = splitBetween(\n    ANIMATE,\n    END,\n    2\n);\nif (__DETECT_ERROR_WEBSITE_6__.length > 0) {\n    console.error(\"Wrong count in website split after animate\");\n}\nexport {\n    WEBSITE_BACKGROUND_OPTIONS,\n    BOX_BORDER_SHADOW,\n    LAYOUT,\n    LAYOUT_COLUMN,\n    LAYOUT_GRID,\n    GRID_COLUMNS,\n    CONTAINER_WIDTH,\n    COVER_PROPERTIES,\n    SCROLL_BUTTON,\n    GRID_IMAGE,\n    TEXT_HIGHLIGHT,\n    CONDITIONAL_VISIBILITY,\n    DEVICE_VISIBILITY,\n};\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { isElement } from \"@html_editor/utils/dom_info\";\nimport { registry } from \"@web/core/registry\";\n\nclass BootstrapOptionPlugin extends Plugin {\n    static id = \"bootstrapOption\";\n    static dependencies = [\"customizeWebsite\"];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        savable_mutation_record_predicates: this.filterBootstrapMutations.bind(this),\n    };\n\n    /**\n     * @param {import(\"@html_editor/core/history_plugin\").HistoryMutationRecord} record\n     */\n    filterBootstrapMutations(record) {\n        // Dropdown attributes to ignore.\n        const dropdownClasses = [\"show\"];\n        const dropdownToggleAttributes = [\"aria-expanded\"];\n        const dropdownMenuAttributes = [\"data-popper-placement\", \"style\", \"data-bs-popper\"];\n        // Offcanvas attributes to ignore.\n        const offcanvasClasses = [\"show\", \"showing\"];\n        const offcanvasAttributes = [\"aria-modal\", \"aria-hidden\", \"role\", \"style\"];\n\n        if (record.type === \"classList\") {\n            // Do not record when showing/hiding a dropdown.\n            if (record.target.matches(\".dropdown-toggle, .dropdown-menu\")) {\n                return !dropdownClasses.includes(record.className);\n            }\n            // Do not record when showing/hiding an offcanvas.\n            if (record.target.matches(\".offcanvas, .offcanvas-backdrop\")) {\n                return !offcanvasClasses.includes(record.className);\n            }\n            return true;\n        }\n        if (record.type === \"attributes\") {\n            // Do not record when showing/hiding a dropdown.\n            if (record.target.matches(\".dropdown-menu\")) {\n                return !dropdownMenuAttributes.includes(record.attributeName);\n            }\n            if (record.target.matches(\".dropdown-toggle\")) {\n                return !dropdownToggleAttributes.includes(record.attributeName);\n            }\n            // Do not record when showing/hiding an offcanvas.\n            if (record.target.matches(\".offcanvas\")) {\n                return !offcanvasAttributes.includes(record.attributeName);\n            }\n            return true;\n        }\n        if (record.type === \"childList\") {\n            const addedOrRemovedNode = (record.addedTrees[0] || record.removedTrees[0]).node;\n            // Do not record the addition/removal of the offcanvas backdrop.\n            return !(\n                isElement(addedOrRemovedNode) && addedOrRemovedNode.matches(\".offcanvas-backdrop\")\n            );\n        }\n        return true;\n    }\n}\n\nregistry.category(\"website-plugins\").add(BootstrapOptionPlugin.id, BootstrapOptionPlugin);\n", "import { useOperation } from \"@html_builder/core/operation_plugin\";\nimport { useDomState } from \"@html_builder/core/utils\";\nimport { Component } from \"@odoo/owl\";\n\nexport class CarouselItemHeaderMiddleButtons extends Component {\n    static template = \"website.CarouselItemHeaderMiddleButtons\";\n    static props = {\n        applyAction: Function,\n        addSlide: Function,\n        removeSlide: Function,\n    };\n\n    setup() {\n        this.callOperation = useOperation();\n        this.state = useDomState((editingElement) => {\n            const carouselItemsNumber = editingElement.parentElement.children.length;\n            return {\n                disableRemove: carouselItemsNumber === 1,\n            };\n        });\n    }\n\n    slide(direction) {\n        const applySpec = {\n            editingElement: this.env.getEditingElement().closest(\".carousel\"),\n            params: {\n                direction: direction,\n            },\n        };\n\n        this.props.applyAction(\"slideCarousel\", applySpec);\n    }\n\n    addSlide() {\n        const carouselEl = this.env.getEditingElement().closest(\".carousel\");\n\n        this.callOperation(async () => {\n            await this.props.addSlide(carouselEl);\n        });\n    }\n\n    removeSlide() {\n        this.callOperation(async () => {\n            await this.props.removeSlide(this.env.getEditingElement());\n        });\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { CarouselItemHeaderMiddleButtons } from \"./carousel_item_header_buttons\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { between } from \"@html_builder/utils/option_sequence\";\nimport { WEBSITE_BACKGROUND_OPTIONS, BOX_BORDER_SHADOW } from \"@website/builder/option_sequence\";\nimport { selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\n/**\n * @typedef { Object } CarouselOptionShared\n * @property { CarouselOptionPlugin['addSlide'] } addSlide\n * @property { CarouselOptionPlugin['removeSlide'] } removeSlide\n * @property { CarouselOptionPlugin['slideCarousel'] } slideCarousel\n */\n\nexport const CAROUSEL_CARDS_SEQUENCE = between(WEBSITE_BACKGROUND_OPTIONS, BOX_BORDER_SHADOW);\n\nconst carouselWrapperSelector =\n    \".s_carousel_wrapper, .s_carousel_intro_wrapper, .s_carousel_cards_wrapper, .s_quotes_carousel_wrapper\";\nconst carouselControlsSelector =\n    \".carousel-control-prev, .carousel-control-next, .carousel-indicators\";\n\nconst carouselItemOptionSelector =\n    \".s_carousel .carousel-item, .s_quotes_carousel .carousel-item, .s_carousel_intro .carousel-item, .s_carousel_cards .carousel-item\";\n\nexport class CarouselOption extends BaseOptionComponent {\n    static template = \"website.CarouselOption\";\n    static selector = \"section\";\n    static exclude =\n        \".s_carousel_intro_wrapper, .s_carousel_cards_wrapper, .s_quotes_carousel_wrapper:has(>.s_quotes_carousel_compact)\";\n    static applyTo = \":scope > .carousel\";\n}\n\nexport class CarouselBottomControllersOption extends BaseOptionComponent {\n    static template = \"website.CarouselBottomControllersOption\";\n    static selector = \"section\";\n    static applyTo = \".s_carousel_intro, .s_quotes_carousel_compact\";\n}\n\nexport class CarouselCardsOption extends BaseOptionComponent {\n    static template = \"website.CarouselCardsOption\";\n    static selector = \"section\";\n    static applyTo = \".s_carousel_cards\";\n}\n\nexport class CarouselOptionPlugin extends Plugin {\n    static id = \"carouselOption\";\n    static dependencies = [\"clone\", \"builderOptions\", \"builderActions\"];\n    static shared = [\"addSlide\", \"removeSlide\", \"slideCarousel\"];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            CarouselOption,\n            CarouselBottomControllersOption,\n            withSequence(CAROUSEL_CARDS_SEQUENCE, CarouselCardsOption),\n        ],\n        builder_header_middle_buttons: {\n            Component: CarouselItemHeaderMiddleButtons,\n            selector: carouselItemOptionSelector,\n            props: {\n                addSlide: (editingElement) => this.addSlide(editingElement),\n                removeSlide: async (editingElement) => {\n                    // Check if the slide is still in the DOM\n                    // TODO: find a more general way to handle target element already removed by an option\n                    if (editingElement.parentElement) {\n                        await this.removeSlide(editingElement.closest(\".carousel\"));\n                    }\n                },\n                applyAction: this.dependencies.builderActions.applyAction,\n            },\n        },\n        container_title: {\n            selector: carouselItemOptionSelector,\n            getTitleExtraInfo: (editingElement) => this.getTitleExtraInfo(editingElement),\n        },\n        builder_actions: {\n            AddSlideAction,\n            SlideCarouselAction,\n            ToggleControllersAction,\n            ToggleCardImgAction,\n        },\n        on_cloned_handlers: this.onCloned.bind(this),\n        on_snippet_dropped_handlers: this.onSnippetDropped.bind(this),\n        get_gallery_items_handlers: this.getGalleryItems.bind(this),\n        reorder_items_handlers: this.reorderCarouselItems.bind(this),\n        before_save_handlers: this.restoreCarousels.bind(this),\n        is_unremovable_selector: carouselItemOptionSelector,\n    };\n\n    /**\n     * Restores all the carousels so their first slide is the active one.\n     */\n    restoreCarousels(rootEl = this.editable) {\n        // Set the first slide as the active one.\n        for (const carouselEl of selectElements(rootEl, \".carousel\")) {\n            carouselEl.querySelectorAll(\".carousel-item\").forEach((itemEl, i) => {\n                itemEl.classList.remove(\"next\", \"prev\", \"left\", \"right\");\n                itemEl.classList.toggle(\"active\", i === 0);\n            });\n            carouselEl.querySelectorAll(\".carousel-indicators > *\").forEach((indicatorEl, i) => {\n                indicatorEl.classList.toggle(\"active\", i === 0);\n                indicatorEl.removeAttribute(\"aria-current\");\n                if (i === 0) {\n                    indicatorEl.setAttribute(\"aria-current\", \"true\");\n                }\n            });\n        }\n    }\n\n    getTitleExtraInfo(editingElement) {\n        const itemEls = [...editingElement.parentElement.children];\n        const activeIndex = itemEls.indexOf(editingElement);\n        // Updates the slide counter.\n        const updatedText = ` (${activeIndex + 1}/${itemEls.length})`;\n        return updatedText;\n    }\n\n    /**\n     * Adds a slide.\n     *\n     * @param {HTMLElement} editingElement the carousel element.\n     */\n    async addSlide(editingElement) {\n        // Clone the active item and remove the \"active\" class.\n        const activeItemEl = editingElement.querySelector(\".carousel-item.active\");\n        const newItemEl = await this.dependencies.clone.cloneElement(activeItemEl, {\n            activateClone: false,\n        });\n        newItemEl.classList.remove(\"active\");\n\n        // Show the controllers (now that there is always more than one item).\n        const controlEls = editingElement.querySelectorAll(carouselControlsSelector);\n        controlEls.forEach((controlEl) => {\n            controlEl.classList.remove(\"d-none\");\n        });\n\n        // Add the new indicator.\n        const indicatorsEl = editingElement.querySelector(\".carousel-indicators\");\n        const newIndicatorEl = this.document.createElement(\"button\");\n        newIndicatorEl.setAttribute(\"data-bs-target\", \"#\" + editingElement.id);\n        newIndicatorEl.setAttribute(\"aria-label\", _t(\"Carousel indicator\"));\n        indicatorsEl.appendChild(newIndicatorEl);\n\n        // Slide to the new item.\n        await this.slide(editingElement, \"next\");\n    }\n\n    /**\n     * Removes the current slide.\n     *\n     * @param {HTMLElement} editingElement the carousel element.\n     */\n    async removeSlide(editingElement) {\n        const itemEls = [...editingElement.querySelectorAll(\".carousel-item\")];\n        const newLength = itemEls.length - 1;\n        if (newLength > 0) {\n            const activeItemEl = editingElement.querySelector(\".carousel-item.active\");\n            const activeIndicatorEl = editingElement.querySelector(\n                \".carousel-indicators > .active\"\n            );\n            // Slide to the previous item.\n            await this.slide(editingElement, \"prev\");\n\n            // Remove the carousel item and the indicator.\n            activeItemEl.remove();\n            activeIndicatorEl.remove();\n\n            // Hide the controllers if there is only one slide left.\n            const controlEls = editingElement.querySelectorAll(carouselControlsSelector);\n            controlEls.forEach((controlEl) =>\n                controlEl.classList.toggle(\"d-none\", newLength === 1)\n            );\n        }\n    }\n\n    /**\n     * Slides the carousel.\n     *\n     * @param {HTMLElement} editingElement the carousel element.\n     * @param {String} direction \"prev\" or \"next\".\n     */\n    async slideCarousel(editingElement, direction) {\n        await this.slide(editingElement, direction);\n    }\n\n    /**\n     * Slides the carousel in the given direction.\n     *\n     * @param {String|Number} direction the direction in which to slide:\n     *     - \"prev\": the previous slide;\n     *     - \"next\": the next slide;\n     *     - number: a slide number.\n     * @param {Element} editingElement the carousel element.\n     * @returns {Promise}\n     */\n    slide(editingElement, direction) {\n        editingElement.addEventListener(\"slide.bs.carousel\", () => {\n            this.slideTimestamp = window.performance.now();\n        });\n\n        return new Promise((resolve) => {\n            editingElement.addEventListener(\n                \"slid.bs.carousel\",\n                () => {\n                    // slid.bs.carousel is most of the time fired too soon by\n                    // bootstrap since it emulates the transitionEnd with a\n                    // setTimeout. We wait here an extra 20% of the time before\n                    // retargeting edition, which should be enough...\n                    const slideDuration = window.performance.now() - this.slideTimestamp;\n                    setTimeout(() => {\n                        // Setting the active indicator manually, as Bootstrap\n                        // could not do it because the `data-bs-slide-to`\n                        // attribute is not here in edit mode anymore.\n                        const itemEls = editingElement.querySelectorAll(\".carousel-item\");\n                        const activeItemEl = editingElement.querySelector(\".carousel-item.active\");\n                        const activeIndex = [...itemEls].indexOf(activeItemEl);\n                        const indicatorEls = editingElement.querySelectorAll(\n                            \".carousel-indicators > *\"\n                        );\n                        const activeIndicatorEl = [...indicatorEls][activeIndex];\n                        activeIndicatorEl.classList.add(\"active\");\n                        activeIndicatorEl.setAttribute(\"aria-current\", \"true\");\n\n                        // Activate the active item.\n                        this.dependencies[\"builderOptions\"].setNextTarget(activeItemEl);\n\n                        resolve();\n                    }, 0.2 * slideDuration);\n                },\n                { once: true }\n            );\n\n            const carouselInstance = window.Carousel.getOrCreateInstance(editingElement, {\n                ride: false,\n                pause: true,\n                keyboard: false,\n            });\n            if (typeof direction === \"number\") {\n                carouselInstance.to(direction);\n            } else {\n                carouselInstance[direction]();\n            }\n        });\n    }\n\n    onCloned({ cloneEl }) {\n        if (cloneEl.matches(carouselWrapperSelector)) {\n            this.assignUniqueID(cloneEl);\n        }\n    }\n\n    onSnippetDropped({ snippetEl }) {\n        if (snippetEl.matches(carouselWrapperSelector)) {\n            this.assignUniqueID(snippetEl);\n        }\n    }\n\n    /**\n     * Creates a unique ID for the carousel and reassign data-attributes that\n     * depend on it.\n     *\n     * @param {HTMLElement} editingElement the carousel element.\n     */\n    assignUniqueID(editingElement) {\n        const id = \"myCarousel\" + Date.now();\n        editingElement.querySelector(\".carousel\").setAttribute(\"id\", id);\n        editingElement.querySelectorAll(\"[data-bs-target]\").forEach((el) => {\n            el.setAttribute(\"data-bs-target\", \"#\" + id);\n        });\n        editingElement.querySelectorAll(\"[data-bs-slide], [data-bs-slide-to]\").forEach((el) => {\n            if (el.hasAttribute(\"data-bs-target\")) {\n                el.setAttribute(\"data-bs-target\", \"#\" + id);\n            } else if (el.hasAttribute(\"href\")) {\n                el.setAttribute(\"href\", \"#\" + id);\n            }\n        });\n    }\n\n    /**\n     * Gets the carousel items to reorder.\n     *\n     * @param {HTMLElement} activeItemEl the current active item\n     * @param {String} optionName\n     * @returns {Array<HTMLElement>}\n     */\n    getGalleryItems(activeItemEl, optionName) {\n        let itemEls = [];\n        if (optionName === \"Carousel\") {\n            const carouselEl = activeItemEl.closest(\".carousel\");\n            itemEls = [...carouselEl.querySelectorAll(\".carousel-item\")];\n        }\n        return itemEls;\n    }\n\n    /**\n     * Updates the DOM with the reordered carousel items.\n     *\n     * @param {HTMLElement} activeItemEl the active item\n     * @param {Array<HTMLElement>} itemEls the reordered items\n     * @param {String} optionName\n     */\n    reorderCarouselItems(activeItemEl, itemEls, optionName) {\n        if (optionName === \"Carousel\") {\n            const carouselEl = activeItemEl.closest(\".carousel\");\n\n            // Replace the content with the new slides.\n            const carouselInnerEl = carouselEl.querySelector(\".carousel-inner\");\n            const newCarouselInnerEl = document.createElement(\"div\");\n            newCarouselInnerEl.classList.add(\"carousel-inner\");\n            newCarouselInnerEl.append(...itemEls);\n            carouselInnerEl.replaceWith(newCarouselInnerEl);\n\n            // Update the indicators.\n            const newPosition = itemEls.indexOf(activeItemEl);\n            updateCarouselIndicators(carouselEl, newPosition);\n\n            // Activate the active slide.\n            this.dependencies.builderOptions.setNextTarget(activeItemEl);\n        }\n    }\n}\n\n/**\n * Updates the carousel indicators to make the one at the given index be the\n * active one.\n *\n * @param {HTMLElement} carouselEl the carousel element\n * @param {Number} newPosition the index\n */\nexport function updateCarouselIndicators(carouselEl, newPosition) {\n    const indicatorEls = carouselEl.querySelectorAll(\".carousel-indicators > *\");\n    indicatorEls.forEach((indicatorEl, i) => {\n        indicatorEl.classList.toggle(\"active\", i === newPosition);\n        indicatorEl.removeAttribute(\"aria-current\");\n        if (i === newPosition) {\n            indicatorEl.setAttribute(\"aria-current\", \"true\");\n        }\n    });\n}\nexport class AddSlideAction extends BuilderAction {\n    static id = \"addSlide\";\n    static dependencies = [\"carouselOption\"];\n    setup() {\n        this.preview = false;\n    }\n    async apply({ editingElement }) {\n        return this.dependencies.carouselOption.addSlide(editingElement);\n    }\n}\nexport class SlideCarouselAction extends BuilderAction {\n    static id = \"slideCarousel\";\n    static dependencies = [\"carouselOption\"];\n    setup() {\n        this.preview = false;\n        this.withLoadingEffect = false;\n    }\n    async apply({ editingElement, params: { direction } }) {\n        await this.dependencies.carouselOption.slideCarousel(editingElement, direction);\n    }\n}\n\nexport class ToggleControllersAction extends BuilderAction {\n    static id = \"toggleControllers\";\n    apply({ editingElement }) {\n        const carouselEl = editingElement.closest(\".carousel\");\n        const indicatorsEl = carouselEl.querySelector(\".carousel-indicators\");\n        const areControllersHidden =\n            carouselEl.classList.contains(\"s_carousel_arrows_hidden\") &&\n            indicatorsEl.classList.contains(\"s_carousel_indicators_hidden\");\n        carouselEl.classList.toggle(\"s_carousel_controllers_hidden\", areControllersHidden);\n    }\n}\nexport class ToggleCardImgAction extends BuilderAction {\n    static id = \"toggleCardImg\";\n    apply({ editingElement }) {\n        const carouselEl = editingElement.closest(\".carousel\");\n        const cardEls = carouselEl.querySelectorAll(\".card\");\n        for (const cardEl of cardEls) {\n            const imageWrapperEl = renderToElement(\"website.s_carousel_cards.imageWrapper\");\n            cardEl.insertAdjacentElement(\"afterbegin\", imageWrapperEl);\n        }\n    }\n    clean({ editingElement: el }) {\n        const carouselEl = el.closest(\".carousel\");\n        carouselEl.querySelectorAll(\"figure\").forEach((el) => el.remove());\n    }\n    isApplied({ editingElement }) {\n        const carouselEl = editingElement.closest(\".carousel\");\n        const cardImgEl = carouselEl.querySelector(\".o_card_img_wrapper\");\n        return !!cardImgEl;\n    }\n}\n\nregistry.category(\"website-plugins\").add(CarouselOptionPlugin.id, CarouselOptionPlugin);\n", "import { CarouselOptionPlugin } from \"./carousel_option_plugin\";\n\nexport class CarouselOptionTranslationPlugin extends CarouselOptionPlugin {\n    static id = \"carouselOption\";\n    static dependencies = [\"builderOptions\", \"builderActions\"];\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\n\nexport class CollapsePlugin extends Plugin {\n    static id = \"collapse\";\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        on_snippet_dropped_handlers: this.onSnippetDropped.bind(this),\n        on_cloned_handlers: this.onCloned.bind(this),\n        dropzone_selector: [\n            {\n                selector: \".accordion-item\",\n                dropLockWithin: \".accordion\",\n            },\n        ],\n        is_movable_selector: {\n            selector: \".s_accordion .accordion-item\",\n            direction: \"vertical\",\n            noScroll: true,\n        },\n    };\n\n    setup() {\n        this.time = new Date().getTime();\n        this.body = this.document.body;\n    }\n\n    onSnippetDropped({ snippetEl }) {\n        const accordionItemsEls = snippetEl.querySelectorAll(\".accordion > .accordion-item\");\n        accordionItemsEls.forEach((accordionItemEl) => {\n            this.createIDs(accordionItemEl);\n        });\n    }\n\n    onCloned({ cloneEl }) {\n        const arrayOfAccordionItemEls = cloneEl.matches(\".accordion > .accordion-item\")\n            ? [cloneEl]\n            : [...cloneEl.querySelectorAll(\".accordion > .accordion-item\")];\n\n        for (const accordionItemEl of arrayOfAccordionItemEls) {\n            this.createIDs(accordionItemEl);\n        }\n    }\n\n    createIDs(editingElement) {\n        const accordionEl = editingElement.closest(\".accordion\");\n        const accordionBtnEl = editingElement.querySelector(\".accordion-button\");\n        const accordionContentEl = editingElement.querySelector('[role=\"region\"]');\n\n        const setUniqueId = (el, label) => {\n            let elemId = el.id;\n            if (!elemId || this.body.querySelectorAll(`#${elemId}`).length > 1) {\n                do {\n                    this.time++;\n                    elemId = `${label}${this.time}`;\n                } while (this.body.querySelector(`#${elemId}`));\n                el.id = elemId;\n            }\n            return elemId;\n        };\n\n        const accordionId = setUniqueId(accordionEl, \"myCollapse\");\n        accordionContentEl.dataset.bsParent = `#${accordionId}`;\n\n        const contentId = setUniqueId(accordionContentEl, \"myCollapseTab\");\n        accordionBtnEl.dataset.bsTarget = `#${contentId}`;\n        accordionBtnEl.setAttribute(\"aria-controls\", contentId);\n\n        const buttonId = setUniqueId(accordionBtnEl, \"myCollapseBtn\");\n        accordionContentEl.setAttribute(\"aria-labelledby\", buttonId);\n    }\n}\n\nregistry.category(\"website-plugins\").add(CollapsePlugin.id, CollapsePlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { isMediaElement } from \"@html_editor/utils/dom_info\";\nimport { selectElements } from \"@html_editor/utils/dom_traversal\";\n\nclass CompanyTeamPlugin extends Plugin {\n    static id = \"companyTeam\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        content_editable_providers: this.getEditableEls.bind(this),\n    };\n\n    getEditableEls(rootEl) {\n        // To fix db in stable\n        const contentEditableEls = [...selectElements(rootEl, \".s_company_team .o_not_editable *\")];\n        return contentEditableEls.filter((el) => isMediaElement(el) || el.tagName === \"IMG\");\n    }\n}\n\nregistry.category(\"website-plugins\").add(CompanyTeamPlugin.id, CompanyTeamPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { CONTAINER_WIDTH } from \"@website/builder/option_sequence\";\nimport { ClassAction } from \"@html_builder/core/core_builder_action_plugin\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class ContentWidthOption extends BaseOptionComponent {\n    static template = \"website.ContentWidthOption\";\n    static selector = \"section, .s_carousel .carousel-item, .s_carousel_intro_item\";\n    static exclude =\n        \"[data-snippet] :not(.oe_structure) > [data-snippet],#footer > *,#o_wblog_post_content *, .s_bento_banner section[data-name='Card'],.s_floating_blocks .s_floating_blocks_block, .s_bento_block_card\";\n    static applyTo = \":scope > .container, :scope > .container-fluid, :scope > .o_container_small\";\n}\n\nclass ContentWidthOptionPlugin extends Plugin {\n    static id = \"contentWidthOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(CONTAINER_WIDTH, ContentWidthOption)],\n        builder_actions: {\n            SetContainerWidthAction,\n        },\n    };\n}\n\nexport class SetContainerWidthAction extends ClassAction {\n    static id = \"setContainerWidth\";\n    apply({ isPreviewing, editingElement }) {\n        super.apply(...arguments);\n        editingElement.classList.toggle(\"o_container_preview\", isPreviewing);\n    }\n}\n\nregistry.category(\"website-plugins\").add(ContentWidthOptionPlugin.id, ContentWidthOptionPlugin);\n", "import { isCSSVariable, setBuilderCSSVariables } from \"@html_builder/utils/utils_css\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { getCSSVariableValue, getHtmlStyle } from \"@html_editor/utils/formatting\";\nimport { parseHTML } from \"@html_editor/utils/html\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { isColorGradient, isCSSColor } from \"@web/core/utils/colors\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { CompositeAction } from \"@html_builder/core/composite_action_plugin\";\n\n/**\n * @typedef { Object } CustomizeWebsiteShared\n * @property { CustomizeWebsitePlugin['customizeWebsiteColors'] } customizeWebsiteColors\n * @property { CustomizeWebsitePlugin['customizeWebsiteVariables'] } customizeWebsiteVariables\n * @property { CustomizeWebsitePlugin['loadTemplateKey'] } loadTemplateKey\n * @property { CustomizeWebsitePlugin['makeSCSSCusto'] } makeSCSSCusto\n * @property { CustomizeWebsitePlugin['toggleTemplate'] } toggleTemplate\n * @property { CustomizeWebsitePlugin['withCustomHistory'] } withCustomHistory\n * @property { CustomizeWebsitePlugin['populateCache'] } populateCache\n * @property { CustomizeWebsitePlugin['loadConfigKey'] } loadConfigKey\n * @property { CustomizeWebsitePlugin['getConfigKey'] } getConfigKey\n * @property { CustomizeWebsitePlugin['getWebsiteVariableValue'] } getWebsiteVariableValue\n * @property { CustomizeWebsitePlugin['getPendingThemeRequests'] } getPendingThemeRequests\n * @property { CustomizeWebsitePlugin['setPendingThemeRequests'] } setPendingThemeRequests\n * @property { CustomizeWebsitePlugin['isPluginDestroyed'] } isPluginDestroyed\n * @property { CustomizeWebsitePlugin['reloadBundles'] } reloadBundles\n * @property { CustomizeWebsitePlugin['setViewsOnSave'] } setViewsOnSave\n */\n\nexport const NO_IMAGE_SELECTION = Symbol.for(\"NoImageSelection\");\n\nexport class CustomizeWebsitePlugin extends Plugin {\n    static id = \"customizeWebsite\";\n    static dependencies = [\"builderActions\", \"history\", \"savePlugin\", \"edit_interaction\"];\n    static shared = [\n        \"customizeWebsiteColors\",\n        \"customizeWebsiteVariables\",\n        \"loadTemplateKey\",\n        \"makeSCSSCusto\",\n        \"toggleTemplate\",\n        \"withCustomHistory\",\n        \"populateCache\",\n        \"loadConfigKey\",\n        \"getConfigKey\",\n        \"getWebsiteVariableValue\",\n        \"getPendingThemeRequests\",\n        \"setPendingThemeRequests\",\n        \"isPluginDestroyed\",\n        \"reloadBundles\",\n        \"setViewsOnSave\",\n    ];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_actions: {\n            CustomizeWebsiteVariableAction,\n            CustomizeWebsiteColorAction,\n            SwitchThemeAction,\n            AddLanguageAction,\n            CustomizeBodyBgTypeAction,\n            CustomizeButtonStyleAction,\n            WebsiteConfigAction,\n            PreviewableWebsiteConfigAction,\n            TemplatePreviewableWebsiteConfigAction,\n            SelectTemplateAction,\n        },\n        color_combination_getters: withSequence(5, (el, actionParam) => {\n            const combination = actionParam.combinationColor;\n            if (combination) {\n                const style = getHtmlStyle(this.document);\n                return `o_cc${getCSSVariableValue(combination, style)}`;\n            }\n        }),\n        save_handlers: this.onSave.bind(this),\n    };\n\n    async onSave() {\n        if (this.viewsToEnableOnSave.size || this.viewsToDisableOnSave.size) {\n            await rpc(\"/website/theme_customize_data\", {\n                is_view_data: true,\n                enable: [...this.viewsToEnableOnSave],\n                disable: [...this.viewsToDisableOnSave],\n                reset_view_arch: false,\n            });\n        }\n    }\n    cache = {};\n    activeRecords = {};\n    activeTemplateViews = {};\n    viewsToEnableOnSave = new Set();\n    viewsToDisableOnSave = new Set();\n    pendingViewRequests = new Set();\n    pendingAssetRequests = new Set();\n    /**\n     * @typedef {{\n     *  isViewData: boolean,\n     *  shouldReset: boolean,\n     *  toEnable: Set<string>,\n     *  toDisable: Set<string>,\n     *  def: Deferred,\n     * }} pendingThemeRequest\n     */\n    /**\n     * @type pendingThemeRequest[]\n     */\n    pendingThemeRequests = [];\n    variablesToCustomize = {};\n    colorsToCustomize = {};\n    resolves = {};\n    getPendingThemeRequests() {\n        return this.pendingThemeRequests;\n    }\n    setPendingThemeRequests(pendingThemeRequests) {\n        this.pendingThemeRequests = pendingThemeRequests;\n    }\n    getWebsiteVariableValue(variable) {\n        const style = getHtmlStyle(this.document);\n        let finalValue = getCSSVariableValue(variable, style);\n        /* TODO dedicated action ?\n        if (!params.colorNames) {\n            return finalValue;\n        }\n        */\n        let tempValue = finalValue;\n        while (tempValue) {\n            finalValue = tempValue;\n            tempValue = getCSSVariableValue(tempValue.replaceAll(\"'\", \"\"), style);\n            if (tempValue === finalValue) {\n                // the CSS variable value is identical to its name.\n                break;\n            }\n        }\n        // Unquote value\n        if (finalValue.startsWith(`'`)) {\n            finalValue = finalValue.substring(1, finalValue.length - 1);\n        }\n        return finalValue;\n    }\n    async customizeWebsiteVariables(\n        variables = {},\n        nullValue = \"null\",\n        clean = false,\n        reloadBundles = true\n    ) {\n        this.variablesToCustomize = Object.assign(this.variablesToCustomize, variables);\n        if (!Object.keys(this.variablesToCustomize).length) {\n            return;\n        }\n        if (clean) {\n            for (const variable in variables) {\n                this.variablesToCustomize[variable] = nullValue;\n            }\n        }\n        await this.debouncedSCSSVariablesCusto(nullValue);\n        if (reloadBundles) {\n            await this.reloadBundles();\n        }\n    }\n    debouncedSCSSVariablesCusto = debounce(async (nullValue) => {\n        const variables = this.variablesToCustomize;\n        this.variablesToCustomize = {};\n        await this.makeSCSSCusto(\n            \"/website/static/src/scss/options/user_values.scss\",\n            variables,\n            nullValue\n        );\n    }, 0);\n    async customizeWebsiteColors(\n        colors = {},\n        { colorType, combinationColor, nullValue, resetCcOnEmpty, reloadBundles = true } = {}\n    ) {\n        const baseURL = \"/website/static/src/scss/options/colors/\";\n        colorType = colorType ? colorType + \"_\" : \"\";\n        const url = `${baseURL}user_${colorType}color_palette.scss`;\n\n        const finalColors = {};\n        for (const [colorName, color] of Object.entries(colors)) {\n            finalColors[colorName] = color;\n            if (color) {\n                const isColorCombination = /^o_cc[12345]$/.test(color);\n                if (isColorCombination) {\n                    finalColors[combinationColor] = parseInt(color.substring(4));\n                    finalColors[colorName] = \"\";\n                } else if (isCSSVariable(color)) {\n                    const customProperty = color.match(/var\\(--(.+?)\\)/)[1];\n                    finalColors[colorName] = this.getWebsiteVariableValue(customProperty);\n                } else if (!isCSSColor(color)) {\n                    finalColors[colorName] = `'${color}'`;\n                }\n            } else {\n                if (resetCcOnEmpty) {\n                    finalColors[combinationColor] = \"\";\n                }\n                finalColors[colorName] = \"\";\n            }\n        }\n        this.colorsToCustomize = Object.assign(this.colorsToCustomize, finalColors);\n        await this.debouncedSCSSColorsCusto(url, nullValue);\n        if (reloadBundles) {\n            await this.reloadBundles();\n        }\n    }\n    debouncedSCSSColorsCusto = debounce(async (url, nullValue) => {\n        const colors = this.colorsToCustomize;\n        this.colorsToCustomize = {};\n        await this.makeSCSSCusto(url, colors, nullValue);\n    }, 0);\n    async makeSCSSCusto(url, values, defaultValue = \"null\") {\n        Object.keys(values).forEach((key) => {\n            values[key] = values[key] || defaultValue;\n        });\n        await this.services.orm.call(\"website.assets\", \"make_scss_customization\", [url, values]);\n    }\n    reloadBundles = debounce(this._reloadBundles.bind(this), 0);\n    async _reloadBundles() {\n        const bundles = await rpc(\"/website/theme_customize_bundle_reload\");\n        const allLinksIframeEls = [];\n        const proms = [];\n        const createLinksProms = (bundleURLs, insertionEl) => {\n            const newLinkEls = [];\n            for (const url of bundleURLs) {\n                const linkEl = this.document.createElement(\"link\");\n                linkEl.setAttribute(\"type\", \"text/css\");\n                linkEl.setAttribute(\"rel\", \"stylesheet\");\n                linkEl.setAttribute(\"href\", `${url}#t=${new Date().getTime()}`); // Ensures that the css will be reloaded.\n                newLinkEls.push(linkEl);\n                proms.push(\n                    new Promise((resolve) => {\n                        linkEl.addEventListener(\"load\", resolve);\n                        linkEl.addEventListener(\"error\", resolve);\n                    })\n                );\n            }\n            for (const el of newLinkEls) {\n                insertionEl.insertAdjacentElement(\"afterend\", el);\n            }\n        };\n        for (const [bundleName, bundleURLs] of Object.entries(bundles)) {\n            const selector = `link[href*=\"${bundleName}\"]`;\n            const linksIframeEls = this.document.querySelectorAll(selector);\n            if (linksIframeEls.length) {\n                allLinksIframeEls.push(...linksIframeEls);\n                createLinksProms(bundleURLs, linksIframeEls[linksIframeEls.length - 1]);\n            }\n        }\n        await Promise.all(proms).then(() => {\n            for (const el of allLinksIframeEls) {\n                el.remove();\n            }\n        });\n        this.dependencies.edit_interaction.restartInteractions();\n    }\n\n    // -------------------------------------------------------------------------\n    // customize website action\n    // -------------------------------------------------------------------------\n    loadConfigKey(actionParam) {\n        const promises = [];\n        for (const paramName of [\"views\", \"assets\"]) {\n            if (actionParam[paramName]) {\n                promises.push(\n                    ...actionParam[paramName].map((record) => {\n                        if (record.startsWith(\"!\")) {\n                            record = record.substring(1);\n                        }\n                        if (!(record in this.cache)) {\n                            this.cache[record] = this._loadBatchKey(record, paramName === \"views\");\n                        }\n                        return this.cache[record];\n                    })\n                );\n            }\n        }\n        return Promise.all(promises);\n    }\n\n    _loadBatchKey(key, isViewData) {\n        const pendingRequests = isViewData ? this.pendingViewRequests : this.pendingAssetRequests;\n        pendingRequests.add(key);\n        return new Promise((resolve) => {\n            this.resolves[key] = resolve;\n            setTimeout(() => {\n                if (pendingRequests.size && !this.isDestroyed) {\n                    const keys = [...pendingRequests];\n                    pendingRequests.clear();\n                    rpc(\"/website/theme_customize_data_get\", {\n                        keys,\n                        is_view_data: isViewData,\n                    }).then((r) => {\n                        if (!this.isDestroyed) {\n                            for (const key of keys) {\n                                this.activeRecords[key] = r.includes(key);\n                                this.resolves[key]();\n                            }\n                        }\n                    });\n                }\n            }, 0);\n        });\n    }\n\n    getConfigKey(key) {\n        if (key.startsWith(\"!\")) {\n            return !this.activeRecords[key.substring(1)];\n        }\n        return this.activeRecords[key];\n    }\n\n    withCustomHistory(action) {\n        const applyFn = action.apply.bind(action);\n        action.apply = async (arg) => {\n            const oldValue = action.getValue(arg);\n            const { value } = arg;\n            const blockedApply = (v) => {\n                this.services.ui.block({ delay: 2500 });\n                return applyFn({ ...arg, value: v })\n                    .then(() => {\n                        this.dispatchTo(\"trigger_dom_updated\");\n                    })\n                    .finally(() => this.services.ui.unblock());\n            };\n            await blockedApply(value);\n            this.dependencies.history.addCustomMutation({\n                apply: () => blockedApply(value),\n                revert: () => blockedApply(oldValue),\n            });\n        };\n    }\n\n    async loadTemplateKey(key) {\n        if (!this.getTemplateKey(key)) {\n            // TODO: make a python method that can return several templates at\n            // once and batch the ORM call.\n            this.activeTemplateViews[key] = await this.services.orm.call(\n                \"ir.ui.view\",\n                \"render_public_asset\",\n                [`${key}`, {}]\n            );\n        }\n        return this.getTemplateKey(key);\n    }\n    toggleTemplate(action, apply) {\n        if (!apply) {\n            // Empty the container and restore the original content\n            action.editingElement.replaceChildren(this.beforePreviewNodes);\n            this.beforePreviewNodes = null;\n            return;\n        }\n\n        if (!this.beforePreviewNodes) {\n            // We are about to apply a template on non-previewed content,\n            // save that content's nodes.\n            this.beforePreviewNodes = [...action.editingElement.childNodes];\n        }\n\n        // Empty the container and add the template content\n        const templateFragment = parseHTML(this.document, this.getTemplateKey(action.params.view));\n        action.editingElement.replaceChildren(templateFragment.firstElementChild);\n    }\n    getTemplateKey(key) {\n        return this.activeTemplateViews[key];\n    }\n    populateCache(record, value) {\n        if (record.startsWith(\"!\")) {\n            record = record.substring(1);\n        }\n        if (!(record in this.cache)) {\n            this.cache[record] = value;\n        }\n        value.then((resolvedValue) => {\n            this.activeRecords[record] = resolvedValue;\n        });\n    }\n    setViewsOnSave(views, to_enable) {\n        const initialViewsToEnableOnSave = new Set(this.viewsToEnableOnSave);\n        const initialViewsToDisableOnSave = new Set(this.viewsToDisableOnSave);\n        for (let view of views) {\n            const toEnable = view.startsWith(\"!\") ? !to_enable : to_enable;\n            view = view.startsWith(\"!\") ? view.substring(1) : view;\n            if (toEnable) {\n                this.viewsToEnableOnSave.add(view);\n                this.viewsToDisableOnSave.delete(view);\n            } else {\n                this.viewsToDisableOnSave.add(view);\n                this.viewsToEnableOnSave.delete(view);\n            }\n        }\n        return () => {\n            // \"Undo\" callback\n            this.viewsToEnableOnSave = initialViewsToEnableOnSave;\n            this.viewsToDisableOnSave = initialViewsToDisableOnSave;\n        };\n    }\n    isPluginDestroyed() {\n        return this.isDestroyed;\n    }\n}\n\nexport class SwitchThemeAction extends BuilderAction {\n    static id = \"switchTheme\";\n    static dependencies = [\"savePlugin\"];\n    setup() {\n        this.preview = false;\n    }\n    async apply() {\n        const save = await new Promise((resolve) => {\n            this.services.dialog.add(ConfirmationDialog, {\n                body: _t(\n                    \"Changing theme requires to leave the editor. This will save all your changes, are you sure you want to proceed? Be careful that changing the theme will reset all your color customizations.\"\n                ),\n                confirm: () => resolve(true),\n                cancel: () => resolve(false),\n            });\n        });\n        if (!save) {\n            return;\n        }\n        // TODO not reload in savePlugin.save ?\n        await this.dependencies.savePlugin.save(/* not in translation */);\n        // TODO doAction in savePlugin.save ?\n        this.services.action.doAction(\"website.theme_install_kanban_action\", {});\n    }\n}\n\nexport class AddLanguageAction extends BuilderAction {\n    static id = \"addLanguage\";\n    static dependencies = [\"savePlugin\"];\n    setup() {\n        this.preview = false;\n    }\n    async apply() {\n        const def = new Deferred();\n        // Retrieve the website id to check by default the website checkbox in\n        // the dialog box 'action_view_base_language_install'\n        const websiteId = this.services.website.currentWebsite.id;\n        const save = await new Promise((resolve) => {\n            this.services.dialog.add(ConfirmationDialog, {\n                body: _t(\n                    \"Adding a language requires to leave the editor. This will save all your changes, are you sure you want to proceed?\"\n                ),\n                confirm: () => resolve(true),\n                cancel: () => resolve(false),\n            });\n        });\n        if (!save) {\n            return;\n        }\n        await this.config.builderSidebar.withHiddenSidebar(() =>\n            this.dependencies.savePlugin.save({\n                shouldSkipAfterSaveHandlers: async () => {\n                    await this.services.action.doAction(\"base.action_view_base_language_install\", {\n                        additionalContext: {\n                            params: {\n                                website_id: websiteId,\n                                url_return: \"[lang]\",\n                            },\n                        },\n                        // The `noReload` in the params of the close callback\n                        // are the only way we have to know whether the modal\n                        // dialog has been cancelled\n                        onClose: (closeParams) => def.resolve(!!closeParams?.noReload),\n                    });\n                    return await def;\n                },\n            })\n        );\n    }\n}\n\nexport class CustomizeBodyBgTypeAction extends BuilderAction {\n    static id = \"customizeBodyBgType\";\n    static dependencies = [\"builderActions\", \"history\", \"customizeWebsite\"];\n    isApplied({ value }) {\n        const getAction = this.dependencies.builderActions.getAction;\n        const currentValue = getAction(\"customizeBodyBgType\").getValue();\n        // NONE has no extra quote, other values have\n        return [`'${value}'`, value].includes(currentValue);\n    }\n    getValue() {\n        const bgImage = getComputedStyle(this.document.querySelector(\"#wrapwrap\"))[\n            \"background-image\"\n        ];\n        if (bgImage === \"none\") {\n            return \"NONE\";\n        }\n        const style = getHtmlStyle(this.document);\n        return getCSSVariableValue(\"body-image-type\", style);\n    }\n    async load({ editingElement: el, params, value, historyImageSrc }) {\n        const getAction = this.dependencies.builderActions.getAction;\n        const oldValue = getAction(\"customizeBodyBgType\").getValue({ params });\n        const oldImageSrc =\n            this.dependencies.customizeWebsite.getWebsiteVariableValue(\"body-image\");\n        let imageSrc = \"\";\n        if (value === \"NONE\") {\n            await this.dependencies.customizeWebsite.customizeWebsiteVariables({\n                \"body-image-type\": \"'image'\",\n                \"body-image\": \"\",\n            });\n        } else {\n            const imageEl = historyImageSrc || (await getAction(\"replaceBgImage\").load({ el }));\n            if (imageEl) {\n                imageSrc = imageEl.src;\n                await this.dependencies.customizeWebsite.customizeWebsiteVariables({\n                    \"body-image-type\": `'${value}'`,\n                    \"body-image\": `'${imageSrc}'`,\n                });\n            } else {\n                imageSrc = NO_IMAGE_SELECTION;\n            }\n        }\n        return { imageSrc, oldImageSrc, oldValue };\n    }\n    apply({ editingElement, params, value, loadResult: { imageSrc, oldImageSrc, oldValue } }) {\n        if (imageSrc === NO_IMAGE_SELECTION) {\n            return;\n        }\n        const getAction = this.dependencies.builderActions.getAction;\n        this.dependencies.history.addCustomMutation({\n            apply: () => {\n                this.services.ui.block({ delay: 2500 });\n                getAction(\"customizeBodyBgType\")\n                    .load({ editingElement, params, value, historyImageSrc: imageSrc })\n                    .then(() => {\n                        this.dispatchTo(\"trigger_dom_updated\");\n                    })\n                    .finally(() => this.services.ui.unblock());\n            },\n            revert: () => {\n                this.services.ui.block({ delay: 2500 });\n                getAction(\"customizeBodyBgType\")\n                    .load({\n                        editingElement,\n                        params,\n                        value: oldValue,\n                        historyImageSrc: oldImageSrc,\n                    })\n                    .then(() => {\n                        this.dispatchTo(\"trigger_dom_updated\");\n                    })\n                    .finally(() => this.services.ui.unblock());\n            },\n        });\n    }\n}\n\nexport class WebsiteConfigAction extends BuilderAction {\n    static id = \"websiteConfig\";\n    static dependencies = [\"builderActions\", \"customizeWebsite\"];\n    setup() {\n        this.reload = {};\n        this.preview = false;\n    }\n    async prepare({ actionParam }) {\n        return this.dependencies.customizeWebsite.loadConfigKey(actionParam);\n    }\n    getPriority({ params }) {\n        const records = [...(params.views || []), ...(params.assets || [])];\n        return records.length;\n    }\n    isApplied({ params }) {\n        const records = [...(params.views || []), ...(params.assets || [])];\n        const configKeysIsApplied = records.every((v) =>\n            this.dependencies.customizeWebsite.getConfigKey(v)\n        );\n        if (params.checkVars || params.checkVars === undefined) {\n            return (\n                configKeysIsApplied &&\n                Object.entries(params.vars || {}).every(\n                    ([variable, value]) =>\n                        value ===\n                        this.dependencies.customizeWebsite.getWebsiteVariableValue(variable)\n                )\n            );\n        }\n        return configKeysIsApplied;\n    }\n    async apply(action) {\n        return this._toggleConfig(action, true);\n    }\n    async clean(action) {\n        return this._toggleConfig(action, false);\n    }\n\n    async _toggleConfig(action, apply) {\n        // step 1: enable and disable records\n        const updateViews = this._toggleTheme(action, \"views\", apply);\n        const updateAssets = this._toggleTheme(action, \"assets\", apply);\n        // step 2: customize vars\n        const updateVars =\n            !apply && action.params.varsOnClean\n                ? this.dependencies.customizeWebsite.customizeWebsiteVariables(\n                      action.params.varsOnClean,\n                      \"null\",\n                      apply\n                  )\n                : action.params.vars\n                ? this.dependencies.customizeWebsite.customizeWebsiteVariables(\n                      action.params.vars,\n                      \"null\",\n                      !apply\n                  )\n                : Promise.resolve();\n        await Promise.all([updateViews, updateAssets, updateVars]);\n        if (this.dependencies.customizeWebsite.isPluginDestroyed()) {\n            return true;\n        }\n    }\n\n    async _toggleTheme(action, paramName, apply) {\n        if (!action.params[paramName]) {\n            return;\n        }\n        const isViewData = paramName === \"views\";\n        const toEnable = new Set();\n        const toDisable = new Set();\n        const prepareRecord = (record, disable) => {\n            if (record.startsWith(\"!\")) {\n                const recordKey = record.substring(1);\n                (disable ? toEnable : toDisable).add(recordKey);\n                (disable ? toDisable : toEnable).delete(recordKey);\n            } else {\n                (disable ? toEnable : toDisable).delete(record);\n                (disable ? toDisable : toEnable).add(record);\n            }\n        };\n        const shouldReset = isViewData && !!action.params.resetViewArch;\n        const records = action.params[paramName] || [];\n        const getAction = this.dependencies.builderActions.getAction;\n        if (action.selectableContext) {\n            if (!apply) {\n                // do nothing, we will do it anyway in the apply call\n                return;\n            }\n            for (const item of action.selectableContext.items) {\n                for (const a of item.getActions()) {\n                    if (getAction(a.actionId) instanceof WebsiteConfigAction) {\n                        for (const record of a.actionParam[paramName] || []) {\n                            // disable all\n                            prepareRecord(record, true);\n                        }\n                    } else if (getAction(a.actionId) instanceof CompositeAction) {\n                        for (const itemAction of a.actionParam.mainParam) {\n                            if (getAction(itemAction.action) instanceof WebsiteConfigAction) {\n                                for (const record of itemAction.actionParam[paramName] || []) {\n                                    prepareRecord(record, true);\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n            for (const record of records) {\n                // enable selected one\n                prepareRecord(record, false);\n            }\n        } else {\n            for (const record of records) {\n                // enable on apply, disable on clear\n                prepareRecord(record, !apply);\n            }\n        }\n        return this._customizeThemeData(isViewData, shouldReset, toEnable, toDisable);\n    }\n\n    /**\n     * Aggregates all sets of records `toEnable` / `toDisable` according to\n     * whether you are enabling/disabling view data and whether it should reset\n     * the arch, so that a RPC call is only done once per tick and per pair\n     * view/reset.\n     *\n     * @param {boolean} isViewData\n     * @param {boolean} shouldReset\n     * @param {Set<string>} toEnable\n     * @param {Set<string>} toDisable\n     * @returns {Promise} deferred function\n     */\n    async _customizeThemeData(isViewData, shouldReset, toEnable, toDisable) {\n        const def = new Deferred();\n        this.dependencies.customizeWebsite.getPendingThemeRequests().push({\n            isViewData,\n            shouldReset,\n            toEnable,\n            toDisable,\n            def,\n        });\n        setTimeout(() => {\n            let aggregatedToEnable = new Set();\n            let aggregatedToDisable = new Set();\n            const defs = [];\n            for (const req of this.dependencies.customizeWebsite.getPendingThemeRequests()) {\n                if (req.isViewData === isViewData && req.shouldReset === shouldReset) {\n                    // Synchronize with the last request: if a view was enabled\n                    // first and then disabled (or the other way around), the\n                    // final state should be disabled (or enabled).\n                    aggregatedToEnable = aggregatedToEnable.difference(req.toDisable);\n                    aggregatedToDisable = aggregatedToDisable.difference(req.toEnable);\n                    // Now aggregate.\n                    aggregatedToEnable = aggregatedToEnable.union(req.toEnable);\n                    aggregatedToDisable = aggregatedToDisable.union(req.toDisable);\n                    defs.push(req.def);\n                }\n            }\n            this.dependencies.customizeWebsite.setPendingThemeRequests(\n                this.dependencies.customizeWebsite\n                    .getPendingThemeRequests()\n                    .filter(\n                        (req) => req.isViewData !== isViewData || req.shouldReset !== shouldReset\n                    )\n            );\n            if (!aggregatedToEnable.size && !aggregatedToDisable.size) {\n                defs.map((def) => def.resolve());\n                return;\n            } else {\n                rpc(\"/website/theme_customize_data\", {\n                    is_view_data: isViewData,\n                    enable: [...aggregatedToEnable],\n                    disable: [...aggregatedToDisable],\n                    reset_view_arch: shouldReset,\n                })\n                    .then(() => Promise.all(defs.map((def) => def.resolve())))\n                    .catch(() => Promise.all(defs.map((def) => def.reject())));\n            }\n        }, 0);\n        return def;\n    }\n}\n\nexport class PreviewableWebsiteConfigAction extends BuilderAction {\n    static id = \"previewableWebsiteConfig\";\n    static dependencies = [\"customizeWebsite\", \"history\"];\n    setup() {\n        // we need this so autoHideMenu recomputes the layout after our changes\n        this.dispatchResize = () => this.window.dispatchEvent(new Event(\"resize\"));\n    }\n    getPriority({ params }) {\n        return (params.previewClass || \"\")?.trim().split(/\\s+/).filter(Boolean).length || 0;\n    }\n    isApplied({ editingElement: el, params }) {\n        if (params.previewClass === undefined || params.previewClass === \"\") {\n            return true;\n        }\n        return params.previewClass.split(/\\s+/).every((cls) => el.classList.contains(cls));\n    }\n    apply({ editingElement: el, isPreviewing, params }) {\n        if (params.previewClass) {\n            params.previewClass.split(/\\s+/).forEach((cls) => el.classList.add(cls));\n        }\n        this.dependencies.history.applyCustomMutation({\n            apply: this.dispatchResize,\n            revert: this.dispatchResize,\n        });\n        if (!isPreviewing) {\n            const viewsToApply = params[\"views\"] || [];\n            let undoApplyCallback;\n            this.dependencies.history.applyCustomMutation({\n                apply: () => {\n                    undoApplyCallback = this.dependencies.customizeWebsite.setViewsOnSave(\n                        viewsToApply,\n                        true\n                    );\n                },\n                revert: () => {\n                    undoApplyCallback();\n                },\n            });\n        }\n    }\n    clean({ editingElement: el, isPreviewing, params }) {\n        if (params.previewClass) {\n            params.previewClass.split(/\\s+/).forEach((cls) => el.classList.remove(cls));\n        }\n        this.dependencies.history.applyCustomMutation({\n            apply: this.dispatchResize,\n            revert: this.dispatchResize,\n        });\n        if (!isPreviewing) {\n            const viewsToClean = params[\"views\"] || [];\n            let undoCleanCallback;\n            this.dependencies.history.applyCustomMutation({\n                apply: () => {\n                    undoCleanCallback = this.dependencies.customizeWebsite.setViewsOnSave(\n                        viewsToClean,\n                        false\n                    );\n                },\n                revert: () => {\n                    undoCleanCallback();\n                },\n            });\n        }\n    }\n}\n\nclass TemplatePreviewableWebsiteConfigAction extends WebsiteConfigAction {\n    static id = \"templatePreviewableWebsiteConfig\";\n\n    setup() {\n        this.reload = {};\n        this.preview = true;\n    }\n\n    async apply(action) {\n        if (!action.isPreviewing) {\n            await super.apply(action);\n        } else {\n            await this.renderPreview(action);\n        }\n    }\n\n    async clean(action) {\n        if (!action.isPreviewing) {\n            await super.clean(action);\n        }\n    }\n\n    async renderPreview({ editingElement: el, params }) {\n        if (params.templateId && !el.closest(params.placeExcludeRootClosest)) {\n            const renderedEl = renderToElement(params.templateId);\n            const targetEl = el;\n            if (targetEl) {\n                if (params.placeBefore) {\n                    for (const el of targetEl.querySelectorAll(params.placeBefore)) {\n                        el.insertAdjacentElement(\"beforebegin\", renderedEl.cloneNode(true));\n                    }\n                }\n                if (params.placeAfter) {\n                    for (const el of targetEl.querySelectorAll(params.placeAfter)) {\n                        el.insertAdjacentElement(\"afterend\", renderedEl.cloneNode(true));\n                    }\n                }\n            }\n        }\n        // Wait one frame to get the proper fade-in animation effect.\n        // The promise ensures this completes before continuing, avoiding a race\n        // that could mark the element o_dirty and trigger an unnecessary save.\n        if (params.previewClass) {\n            await new Promise((resolve) => {\n                requestAnimationFrame(() => {\n                    params.previewClass.split(/\\s+/).forEach((cls) => el.classList.add(cls));\n                    resolve();\n                });\n            });\n        }\n    }\n}\n\nexport class SelectTemplateAction extends BuilderAction {\n    static id = \"selectTemplate\";\n    static dependencies = [\"customizeWebsite\"];\n    async prepare({ actionParam }) {\n        return await this.dependencies.customizeWebsite.loadTemplateKey(actionParam.view);\n    }\n    isApplied({ editingElement, params: { templateClass } }) {\n        if (templateClass) {\n            return !!editingElement.querySelector(`.${templateClass}`);\n        }\n        return true;\n    }\n    async apply(action) {\n        return this.dependencies.customizeWebsite.toggleTemplate(action, true);\n    }\n    clean(action) {\n        return this.dependencies.customizeWebsite.toggleTemplate(action, false);\n    }\n}\n\nexport class CustomizeWebsiteVariableAction extends BuilderAction {\n    static id = \"customizeWebsiteVariable\";\n    static dependencies = [\"customizeWebsite\"];\n    setup() {\n        this.preview = false;\n        this.dependencies.customizeWebsite.withCustomHistory(this);\n    }\n    isApplied({ params: { mainParam: variable } = {}, value }) {\n        const currentValue = this.dependencies.customizeWebsite.getWebsiteVariableValue(variable);\n        return (\n            // There might be unquoted values in existing databases.\n            currentValue === value || `'${currentValue}'` === value\n        );\n    }\n    getValue({ params: { mainParam: variable } }) {\n        const currentValue = this.dependencies.customizeWebsite.getWebsiteVariableValue(variable);\n        return currentValue;\n    }\n    async apply({ params: { mainParam: variable, nullValue = \"null\" }, value }) {\n        await this.dependencies.customizeWebsite.customizeWebsiteVariables(\n            {\n                [variable]: value,\n            },\n            nullValue\n        );\n    }\n}\n\nexport class CustomizeWebsiteColorAction extends BuilderAction {\n    static id = \"customizeWebsiteColor\";\n    static dependencies = [\"customizeWebsite\"];\n    setup() {\n        this.preview = false;\n        this.dependencies.customizeWebsite.withCustomHistory(this);\n    }\n    getValue({ params: { mainParam: color, colorType, gradientColor, combinationColor } }) {\n        const style = getHtmlStyle(this.document);\n        if (gradientColor) {\n            const gradientValue =\n                this.dependencies.customizeWebsite.getWebsiteVariableValue(gradientColor);\n            if (gradientValue) {\n                // Pass through style to restore rgb/a which might\n                // have been lost during SCSS generation process.\n                // TODO Remove this once colorpicker will be able\n                // to cope with #rrggbb gradient color elements.\n                const el = document.createElement(\"div\");\n                el.style.setProperty(\"background-image\", gradientValue);\n                return el.style.getPropertyValue(\"background-image\");\n            }\n        }\n        return getCSSVariableValue(color, style);\n    }\n    async apply({\n        params: { mainParam: color, colorType, gradientColor, combinationColor, nullValue },\n        value,\n    }) {\n        if (gradientColor) {\n            let colorValue = \"\";\n            let gradientValue = \"\";\n            if (isColorGradient(value)) {\n                gradientValue = value;\n            } else {\n                colorValue = value;\n            }\n            const isColorCombination = /^o_cc[12345]$/.test(value);\n            await this.dependencies.customizeWebsite.customizeWebsiteColors(\n                {\n                    [color]: colorValue,\n                },\n                {\n                    colorType,\n                    combinationColor,\n                    nullValue,\n                    // Do not touch CC if a gradient is being set\n                    resetCcOnEmpty: !gradientValue,\n                    // Reload bundle will be handled by setting gradient\n                    reloadBundles: false,\n                }\n            );\n            await this.dependencies.customizeWebsite.customizeWebsiteVariables({\n                [gradientColor]: isColorCombination ? nullValue : gradientValue || nullValue,\n            }); // reloads bundles\n        } else {\n            await this.dependencies.customizeWebsite.customizeWebsiteColors(\n                { [color]: value },\n                { colorType, combinationColor, resetCcOnEmpty: true, nullValue }\n            );\n        }\n        setBuilderCSSVariables(getHtmlStyle(this.document));\n    }\n}\n\nexport class CustomizeButtonStyleAction extends BuilderAction {\n    static id = \"customizeButtonStyle\";\n    static dependencies = [\"customizeWebsite\"];\n    setup() {\n        this.preview = false;\n        this.dependencies.customizeWebsite.withCustomHistory(this);\n    }\n    isApplied({ params, value }) {\n        return this.getValue({ params }) === value;\n    }\n    getValue({ params: { mainParam: which } }) {\n        const style = getHtmlStyle(this.document);\n        const isOutline = getCSSVariableValue(`btn-${which}-outline`, style);\n        const isFlat = getCSSVariableValue(`btn-${which}-flat`, style);\n        return isFlat === \"true\" ? \"flat\" : isOutline === \"true\" ? \"outline\" : \"fill\";\n    }\n    async apply({ params: { mainParam: which, nullValue }, value }) {\n        await this.dependencies.customizeWebsite.customizeWebsiteVariables(\n            {\n                [`btn-${which}-outline`]: value === \"outline\" ? \"true\" : \"false\",\n                [`btn-${which}-flat`]: value === \"flat\" ? \"true\" : \"false\",\n            },\n            nullValue\n        );\n    }\n}\n\nregistry.category(\"website-plugins\").add(CustomizeWebsitePlugin.id, CustomizeWebsitePlugin);\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class DynamicSvgOption extends BaseOptionComponent {\n    static template = \"website.DynamicSvgOption\";\n    static selector = \"img[src^='/html_editor/shape/'], img[src^='/web_editor/shape/']\";\n\n    setup() {\n        super.setup();\n        this.title = {\n            c1: _t(\"Change primary color\"),\n            c2: _t(\"Change secondary color\"),\n            c3: _t(\"Change color\"),\n            c4: _t(\"Change accent color\"),\n            c5: _t(\"Change color\"),\n        };\n        this.domState = useDomState((imgEl) => {\n            const colors = {};\n            const searchParams = new URL(imgEl.src, window.location.origin).searchParams;\n            for (const colorName of [\"c1\", \"c2\", \"c3\", \"c4\", \"c5\"]) {\n                const color = searchParams.get(colorName);\n                if (color) {\n                    colors[colorName] = color;\n                }\n            }\n            return {\n                colors: colors,\n            };\n        });\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { DynamicSvgOption } from \"./dynamic_svg_option\";\nimport { normalizeCSSColor } from \"@web/core/utils/colors\";\nimport { getCSSVariableValue, getHtmlStyle } from \"@html_editor/utils/formatting\";\nimport { loadImage } from \"@html_editor/utils/image_processing\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { DYNAMIC_SVG } from \"@html_builder/utils/option_sequence\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\n\nclass DynamicSvgOptionPlugin extends Plugin {\n    static id = \"DynamicSvgOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(DYNAMIC_SVG, DynamicSvgOption)],\n        builder_actions: {\n            SvgColorAction,\n        },\n    };\n}\n\nexport class SvgColorAction extends BuilderAction {\n    static id = \"svgColor\";\n    getValue({ editingElement: imgEl, params: { mainParam: colorName } }) {\n        const searchParams = new URL(imgEl.src, window.location.origin).searchParams;\n        const color = searchParams.get(colorName);\n        return /^o-color-[1-5]$/.test(color)\n            ? getCSSVariableValue(color, getHtmlStyle(this.document))\n            : normalizeCSSColor(color);\n    }\n    colorToSearchParams(color) {\n        const cssVarMatch = color.match(/var\\(--(.+)\\)/);\n        if (cssVarMatch === null) {\n            return normalizeCSSColor(color);\n        }\n        // If it is a palette color, return the variable name\n        if (/^o-color-[1-5]$/.test(cssVarMatch[1])) {\n            return cssVarMatch[1];\n        }\n        // If it is a CSS variable, extract the color value\n        return getCSSVariableValue(cssVarMatch[1], getHtmlStyle(this.document));\n    }\n    async load({ editingElement: imgEl, params: { mainParam: colorName }, value: color }) {\n        const newURL = new URL(imgEl.src, window.location.origin);\n        newURL.searchParams.set(colorName, this.colorToSearchParams(color));\n        const src = newURL.pathname + newURL.search;\n        await loadImage(src);\n        return src;\n    }\n    apply({\n        editingElement: imgEl,\n        params: { mainParam: colorName },\n        value: color,\n        loadResult: newSrc,\n    }) {\n        imgEl.setAttribute(\"src\", newSrc);\n    }\n}\n\nregistry.category(\"website-plugins\").add(DynamicSvgOptionPlugin.id, DynamicSvgOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\n\n/**\n * @typedef { Object } EditInteractionShared\n * @property { EditInteractionPlugin['restartInteractions'] } restartInteractions\n * @property { EditInteractionPlugin['stopInteractions'] } stopInteractions\n */\n\n/**\n * @typedef {((commonAncestorEl: HTMLElement) => void)[]} content_manually_updated_handlers\n */\n\nexport class EditInteractionPlugin extends Plugin {\n    static id = \"edit_interaction\";\n\n    static shared = [\"restartInteractions\", \"stopInteraction\"];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        normalize_handlers: this.refreshInteractions.bind(this),\n        content_manually_updated_handlers: this.refreshInteractions.bind(this),\n        before_save_handlers: withSequence(5, this.stopInteractions.bind(this)),\n        after_save_handlers: this.restartInteractions.bind(this),\n        on_will_clone_handlers: ({ originalEl }) => {\n            this.stopInteractions(originalEl);\n        },\n        on_cloned_handlers: ({ originalEl }) => {\n            this.restartInteractions(originalEl);\n            // The clonedEl is implicitly started because it is a newly\n            // inserted content.\n        },\n    };\n\n    setup() {\n        this.websiteEditService = undefined;\n\n        window.parent.document.addEventListener(\n            \"transfer_website_edit_service\",\n            this.updateEditInteraction.bind(this),\n            { once: true }\n        );\n        const event = new CustomEvent(\"edit_interaction_plugin_loaded\");\n        event.shared = this.__editor.shared;\n        window.parent.document.dispatchEvent(event);\n    }\n    destroy() {\n        this.websiteEditService?.uninstallPatches?.();\n        this.stopInteractions();\n    }\n\n    updateEditInteraction({ detail: { websiteEditService } }) {\n        this.websiteEditService = websiteEditService;\n        this.websiteEditService.installPatches();\n    }\n\n    restartInteractions(element) {\n        if (!this.websiteEditService) {\n            throw new Error(\"website edit service not loaded\");\n        }\n        this.websiteEditService.update(element, \"edit\");\n    }\n\n    refreshInteractions(element) {\n        this.websiteEditService.refresh(element);\n    }\n\n    stopInteractions(element) {\n        if (!this.websiteEditService) {\n            throw new Error(\"website edit service not loaded\");\n        }\n        this.websiteEditService.stop(element);\n    }\n\n    stopInteraction(name) {\n        if (!this.websiteEditService) {\n            throw new Error(\"website edit service not loaded\");\n        }\n        this.websiteEditService.stopInteraction(name);\n    }\n}\n\nregistry.category(\"website-plugins\").add(EditInteractionPlugin.id, EditInteractionPlugin);\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\n\nexport class FloatingBlocksBlockMobileOption extends BaseOptionComponent {\n    static template = \"website.FloatingBlocksBlockMobileOption\";\n    static selector = \".s_floating_blocks .s_floating_blocks_block\";\n    static applyTo = \".container-fluid\";\n    setup() {\n        super.setup();\n        this.state = useDomState((editingElement) => ({\n            isMobileView: this.env.editor.config.isMobileView(editingElement),\n        }));\n    }\n}\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { BorderConfigurator } from \"@html_builder/plugins/border_configurator_option\";\nimport { AddElementOption } from \"@website/builder/plugins/layout_option/add_element_option\";\n\nexport class FloatingBlocksBlockOption extends BaseOptionComponent {\n    static template = \"website.FloatingBlocksBlockOption\";\n    static components = {\n        BorderConfigurator,\n        AddElementOption,\n    };\n    static selector = \".s_floating_blocks .s_floating_blocks_block\";\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { BEGIN } from \"@html_builder/utils/option_sequence\";\nimport { LAYOUT_GRID } from \"@website/builder/option_sequence\";\nimport { FloatingBlocksBlockOption } from \"./floating_blocks_block_option\";\nimport { FloatingBlocksBlockMobileOption } from \"./floating_blocks_block_mobile_option\";\n\nclass FloatingBlocksBlockOptionPlugin extends Plugin {\n    static id = \"floatingBlocksBlockOptionPlugin\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(BEGIN, FloatingBlocksBlockMobileOption),\n            withSequence(LAYOUT_GRID, FloatingBlocksBlockOption),\n        ],\n        dropzone_selector: [\n            // Lock grid-items within their grid\n            {\n                selector: \".s_floating_blocks_block_grid .o_grid_item\",\n                dropLockWithin: \".s_floating_blocks_block_grid\",\n            },\n            // Lock block-items within the snippet\n            {\n                selector: \".s_floating_blocks .s_floating_blocks_block\",\n                dropLockWithin: \".s_floating_blocks\",\n                dropNear: \".s_floating_blocks .s_floating_blocks_block\",\n            },\n        ],\n    };\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(FloatingBlocksBlockOptionPlugin.id, FloatingBlocksBlockOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { after } from \"@html_builder/utils/option_sequence\";\nimport { DEVICE_VISIBILITY } from \"@website/builder/option_sequence\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class FloatingBlocksOption extends BaseOptionComponent {\n    static template = \"website.FloatingBlocksOption\";\n    static selector = \".s_floating_blocks\";\n}\n\nexport class FloatingBlocksOptionPlugin extends Plugin {\n    static id = \"floatingBlocksOptionPlugin\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(after(DEVICE_VISIBILITY), FloatingBlocksOption)],\n        builder_actions: {\n            FloatingBlocksRoundnessAction,\n            AddFloatingBlockCardAction,\n        },\n    };\n}\n\nexport class FloatingBlocksRoundnessAction extends BuilderAction {\n    static id = \"floatingBlocksRoundness\";\n    getValue({ editingElement }) {\n        for (let x = 0; x <= 5; x++) {\n            if (editingElement.classList.contains(`rounded-${x}`)) {\n                return x;\n            }\n        }\n        return 0;\n    }\n    apply({ editingElement, value }) {\n        for (let x = 0; x <= 5; x++) {\n            editingElement.classList.remove(`rounded-${x}`);\n        }\n        editingElement.classList.add(`rounded-${value}`);\n    }\n}\nexport class AddFloatingBlockCardAction extends BuilderAction {\n    static id = \"addFloatingBlockCard\";\n    static dependencies = [\"builderOptions\"];\n    apply({ editingElement: el }) {\n        const newCardEl = renderToElement(\"website.s_floating_blocks.new_card\");\n        const wrapperEl = el.querySelector(\".s_floating_blocks_wrapper\");\n        wrapperEl.appendChild(newCardEl);\n        newCardEl.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n        this.dependencies.builderOptions.setNextTarget(newCardEl);\n    }\n}\n\nregistry.category(\"website-plugins\").add(FloatingBlocksOptionPlugin.id, FloatingBlocksOptionPlugin);\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { Component, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nclass GoogleFontAutoComplete extends AutoComplete {\n    setup() {\n        super.setup();\n        this.inputRef = useRef(\"input\");\n        this.sourcesListRef = useRef(\"sourcesList\");\n        useEffect(\n            (el) => {\n                el.setAttribute(\"id\", \"google_font\");\n            },\n            () => [this.inputRef.el]\n        );\n    }\n\n    get dropdownOptions() {\n        return {\n            ...super.dropdownOptions,\n            position: \"bottom-fit\",\n        };\n    }\n\n    onInput(ev) {\n        super.onInput(ev);\n        if (this.sourcesListRef.el) {\n            this.sourcesListRef.el.scrollTop = 0;\n        }\n    }\n}\n\nexport class AddFontDialog extends Component {\n    static template = \"website.dialog.addFont\";\n    static components = { GoogleFontAutoComplete, Dialog };\n    static props = {\n        close: Function,\n        allFonts: Array,\n        googleFonts: Array,\n        googleLocalFonts: Array,\n        uploadedLocalFonts: Array,\n        variable: String,\n        customize: Function,\n        reloadEditor: Function,\n    };\n    state = useState({\n        valid: true,\n        loading: false,\n        googleFontFamily: undefined,\n        googleServe: !this.env.services.website.currentWebsite.cookies_bar,\n        uploadedFontName: undefined,\n        uploadedFonts: [],\n        uploadedFontFaces: undefined,\n        previewText: _t(\"The quick brown fox jumps over the lazy dog.\"),\n    });\n    setup() {\n        this.fileInput = useRef(\"fileInput\");\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n    }\n\n    async onClickSave() {\n        if (this.state.loading) {\n            return;\n        }\n        this.state.loading = true;\n        const shouldClose = await this.save(this.state);\n        if (shouldClose) {\n            this.props.close();\n            return;\n        }\n        this.state.loading = false;\n    }\n    onClickCancel() {\n        this.props.close();\n    }\n    get getGoogleFontList() {\n        return [\n            {\n                options: async (term) => {\n                    if (!this.googleFontList) {\n                        await rpc(\"/website/google_font_metadata\").then((data) => {\n                            this.googleFontList = data.familyMetadataList.map(\n                                (font) => font.family\n                            );\n                        });\n                    }\n                    const lowerCaseTerm = term.toLowerCase();\n                    const filtered = this.googleFontList.filter((value) =>\n                        value.toLowerCase().includes(lowerCaseTerm)\n                    );\n                    return filtered.map((fontFamilyName) => ({\n                        label: fontFamilyName,\n                        onSelect: () => this.onGoogleFontSelect(fontFamilyName),\n                    }));\n                },\n            },\n        ];\n    }\n    async onGoogleFontSelect(fontFamily) {\n        this.fileInput.el.value = \"\";\n        this.state.uploadedFonts = [];\n        this.state.uploadedFontName = undefined;\n        this.state.uploadedFontFaces = undefined;\n        try {\n            const result = await fetch(\n                `https://fonts.googleapis.com/css?family=${encodeURIComponent(\n                    fontFamily\n                )}:300,300i,400,400i,700,700i`,\n                { method: \"HEAD\" }\n            );\n            // Google fonts server returns a 400 status code if family is not valid.\n            if (result.ok) {\n                const linkId = `previewFont${fontFamily}`;\n                if (!document.querySelector(`link[id='${linkId}']`)) {\n                    const linkEl = document.createElement(\"link\");\n                    linkEl.id = linkId;\n                    linkEl.setAttribute(\"href\", result.url);\n                    linkEl.setAttribute(\"rel\", \"stylesheet\");\n                    linkEl.dataset.fontPreview = true;\n                    document.head.appendChild(linkEl);\n                }\n                this.state.googleFontFamily = fontFamily;\n            } else {\n                this.state.googleFontFamily = undefined;\n            }\n        } catch (error) {\n            console.error(error);\n        }\n    }\n    async onUploadChange(e) {\n        this.state.googleFontFamily = undefined;\n        const file = this.fileInput.el.files[0];\n        if (!file) {\n            this.state.uploadedFonts = [];\n            this.state.uploadedFontName = undefined;\n            this.state.uploadedFontFaces = undefined;\n            return;\n        }\n        const reader = new FileReader();\n        reader.onload = (e) => {\n            const base64 = e.target.result.split(\",\")[1];\n            rpc(\"/website/theme_upload_font\", {\n                name: file.name,\n                data: base64,\n            }).then((result) => {\n                this.state.uploadedFonts = result;\n                this.updateFontStyle(file.name.substr(0, file.name.lastIndexOf(\".\")));\n            });\n        };\n        reader.readAsDataURL(file);\n    }\n    /**\n     * Deduces the style of uploaded fonts and creates inline style\n     * elements in the backend iframe's head to make the font-faces\n     * available for preview.\n     *\n     * @param baseFontName\n     */\n    updateFontStyle(baseFontName) {\n        const targetFonts = {};\n        // Add candidate tags to fonts.\n        let shortestNamedFont;\n        for (const font of this.state.uploadedFonts) {\n            if (!shortestNamedFont || font.name.length < shortestNamedFont.name.length) {\n                shortestNamedFont = font;\n            }\n            font.isItalic = /italic/i.test(font.name);\n            font.isLight = /light|300/i.test(font.name);\n            font.isBold = /bold|700/i.test(font.name);\n            font.isRegular = /regular|400/i.test(font.name);\n            font.weight = font.isRegular ? 400 : font.isLight ? 300 : font.isBold ? 700 : undefined;\n            if (font.isItalic && !font.weight) {\n                if (!/00|thin|medium|black|condense|extrude/i.test(font.name)) {\n                    font.isRegular = true;\n                    font.weight = 400;\n                }\n            }\n            font.style = font.isItalic ? \"italic\" : \"normal\";\n            if (font.weight) {\n                targetFonts[`${font.weight}${font.style}`] = font;\n            }\n        }\n        if (!Object.values(targetFonts).filter((font) => font.isRegular).length) {\n            // Keep font with shortest name.\n            shortestNamedFont.weight = 400;\n            shortestNamedFont.style = \"normal\";\n            targetFonts[\"400\"] = shortestNamedFont;\n        }\n        const fontFaces = [];\n        for (const font of Object.values(targetFonts)) {\n            fontFaces.push(`@font-face{\n                font-family: ${baseFontName};\n                font-style: ${font.style};\n                font-weight: ${font.weight};\n                src:url(\"${font.url}\");\n            }`);\n        }\n        let styleEl = document.head.querySelector(\n            `style[id='WebsiteThemeFontPreview-${baseFontName}']`\n        );\n        if (!styleEl) {\n            styleEl = document.createElement(\"style\");\n            styleEl.id = `WebsiteThemeFontPreview-${baseFontName}`;\n            styleEl.dataset.fontPreview = true;\n            document.head.appendChild(styleEl);\n        }\n        const previewFontFaces = fontFaces.join(\"\");\n        styleEl.textContent = previewFontFaces;\n        this.state.uploadedFontName = baseFontName;\n        this.state.uploadedFontFaces = previewFontFaces;\n    }\n    async save(state) {\n        const uploadedFontName = state.uploadedFontName;\n        const uploadedFontFaces = state.uploadedFontFaces;\n        let font = undefined;\n        if (uploadedFontName && uploadedFontFaces) {\n            const fontExistsLocally = this.props.uploadedLocalFonts.some(\n                (localFont) => localFont.split(\":\")[0] === `'${uploadedFontName}'`\n            );\n            if (fontExistsLocally) {\n                this.dialog.add(ConfirmationDialog, {\n                    title: _t(\"Font exists\"),\n                    body: _t(\n                        \"This uploaded font already exists.\\nTo replace an existing font, remove it first.\"\n                    ),\n                });\n                return;\n            }\n            const homonymGoogleFontExists =\n                this.props.googleFonts.some((font) => font === uploadedFontName) ||\n                this.props.googleLocalFonts.some(\n                    (font) => font.split(\":\")[0] === `'${uploadedFontName}'`\n                );\n            if (homonymGoogleFontExists) {\n                this.dialog.add(ConfirmationDialog, {\n                    title: _t(\"Font name already used\"),\n                    body: _t(\n                        \"A font with the same name already exists.\\nTry renaming the uploaded file.\"\n                    ),\n                });\n                return;\n            }\n            // Create attachment.\n            const [fontCssId] = await this.orm.call(\"ir.attachment\", \"create_unique\", [\n                [\n                    {\n                        name: uploadedFontName,\n                        description: `CSS font face for ${uploadedFontName}`,\n                        datas: btoa(uploadedFontFaces),\n                        res_model: \"ir.attachment\",\n                        mimetype: \"text/css\",\n                        public: true,\n                    },\n                ],\n            ]);\n            this.props.uploadedLocalFonts.push(`'${uploadedFontName}': ${fontCssId}`);\n            font = uploadedFontName;\n        } else {\n            let isValidFamily = false;\n            font = state.googleFontFamily;\n\n            try {\n                const result = await fetch(\n                    \"https://fonts.googleapis.com/css?family=\" +\n                        encodeURIComponent(font) +\n                        \":300,300i,400,400i,700,700i\",\n                    { method: \"HEAD\" }\n                );\n                // Google fonts server returns a 400 status code if family is not valid.\n                if (result.ok) {\n                    isValidFamily = true;\n                }\n            } catch (error) {\n                console.error(error);\n            }\n\n            if (!isValidFamily) {\n                this.dialog.add(ConfirmationDialog, {\n                    title: _t(\"Font access\"),\n                    body: _t(\"The selected font cannot be accessed.\"),\n                });\n                return;\n            }\n\n            const googleFontServe = state.googleServe;\n            const fontName = `'${font}'`;\n            // If the font already exists, it will only be added if\n            // the user chooses to add it locally when it is already\n            // imported from the Google Fonts server.\n            const fontExistsLocally = this.props.googleLocalFonts.some(\n                (localFont) => localFont.split(\":\")[0] === fontName\n            );\n            const fontExistsOnServer = this.props.allFonts.includes(fontName);\n            const preventFontAddition =\n                fontExistsLocally || (fontExistsOnServer && googleFontServe);\n            if (preventFontAddition) {\n                this.dialog.add(ConfirmationDialog, {\n                    title: _t(\"Font exists\"),\n                    body: _t(\n                        \"This font already exists, you can only add it as a local font to replace the server version.\"\n                    ),\n                });\n                return;\n            }\n            if (googleFontServe) {\n                this.props.googleFonts.push(font);\n            } else {\n                this.props.googleLocalFonts.push(`'${font}': ''`);\n            }\n        }\n        await this.props.customize({\n            values: { [this.props.variable]: `'${font}'` },\n            googleFonts: this.props.googleFonts,\n            googleLocalFonts: this.props.googleLocalFonts,\n            uploadedLocalFonts: this.props.uploadedLocalFonts,\n        });\n        const styleEl = document.head.querySelector(`[id='WebsiteThemeFontPreview-${font}']`);\n        if (styleEl) {\n            delete styleEl.dataset.fontPreview;\n        }\n        await this.props.reloadEditor();\n        return true;\n    }\n}\n\nexport function showAddFontDialog(dialog, fontsData, variable, customize, reloadEditor) {\n    dialog.add(\n        AddFontDialog,\n        {\n            allFonts: fontsData.allFonts,\n            googleFonts: fontsData.googleFonts,\n            googleLocalFonts: fontsData.googleLocalFonts,\n            uploadedLocalFonts: fontsData.uploadedLocalFonts,\n            variable,\n            customize,\n            reloadEditor,\n        },\n        {\n            onClose: () => {\n                for (const el of document.head.querySelectorAll(\"[data-font-preview]\")) {\n                    el.remove();\n                }\n            },\n        }\n    );\n}\n", "import { registry } from \"@web/core/registry\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { getCSSVariableValue, getHtmlStyle } from \"@html_editor/utils/formatting\";\nimport { showAddFontDialog } from \"./add_font_dialog\";\n\n/**\n * @typedef { Object } WebsiteFontShared\n * @property { WebsiteFontPlugin['addFont'] } addFont\n * @property { WebsiteFontPlugin['deleteFont'] } deleteFont\n */\n\n// TODO Website-specific\nclass WebsiteFontPlugin extends Plugin {\n    static id = \"websiteFont\";\n    static shared = [\"addFont\", \"deleteFont\"];\n    static dependencies = [\"savePlugin\", \"builderFont\", \"customizeWebsite\"];\n\n    async addFont(variable) {\n        const fontsData = await this.dependencies.builderFont.getFontsData();\n        showAddFontDialog(\n            this.services.dialog,\n            fontsData,\n            variable,\n            this.customizeFonts.bind(this),\n            this.config.reloadEditor\n        );\n    }\n    async customizeFonts({ values = {}, googleFonts, googleLocalFonts, uploadedLocalFonts }) {\n        if (googleFonts.length) {\n            values[\"google-fonts\"] = \"('\" + googleFonts.join(\"', '\") + \"')\";\n        } else {\n            values[\"google-fonts\"] = \"null\";\n        }\n        if (googleLocalFonts.length) {\n            values[\"google-local-fonts\"] = \"(\" + googleLocalFonts.join(\", \") + \")\";\n        } else {\n            values[\"google-local-fonts\"] = \"null\";\n        }\n        if (uploadedLocalFonts.length) {\n            values[\"uploaded-local-fonts\"] = \"(\" + uploadedLocalFonts.join(\", \") + \")\";\n        } else {\n            values[\"uploaded-local-fonts\"] = \"null\";\n        }\n        await this.dependencies.customizeWebsite.makeSCSSCusto(\n            \"/website/static/src/scss/options/user_values.scss\",\n            values\n        );\n        this.dependencies.builderFont.getFontsCache().invalidate();\n        // TODO reloadEditor: true\n        await this.dependencies.savePlugin.save(/* not in translation */);\n    }\n    async deleteFont(font) {\n        const { googleFonts, googleLocalFonts, uploadedLocalFonts } =\n            await this.dependencies.builderFont.getFontsData();\n        const values = {};\n\n        // Remove Google font\n        const fontIndex = font.indexForType;\n        const localFont = font.type;\n        let fontName;\n        if (localFont === \"uploaded\") {\n            const font = uploadedLocalFonts[fontIndex].split(\":\");\n            // Remove double quotes\n            fontName = font[0].substring(1, font[0].length - 1);\n            values[\"delete-font-attachment-id\"] = font[1];\n            uploadedLocalFonts.splice(fontIndex, 1);\n        } else if (localFont === \"google\") {\n            const googleFont = googleLocalFonts[fontIndex].split(\":\");\n            // Remove double quotes\n            fontName = googleFont[0].substring(1, googleFont[0].length - 1);\n            values[\"delete-font-attachment-id\"] = googleFont[1];\n            googleLocalFonts.splice(fontIndex, 1);\n        } else {\n            fontName = googleFonts[fontIndex];\n            googleFonts.splice(fontIndex, 1);\n        }\n\n        // Adapt font variable indexes to the removal\n        const style = getHtmlStyle(this.document);\n        this.getResource(\"fontCssVariables\").forEach((variable) => {\n            const value = getCSSVariableValue(variable, style);\n            if (value.substring(1, value.length - 1) === fontName) {\n                // If an element is using the google font being removed, reset\n                // it to the theme default.\n                values[variable] = \"null\";\n            }\n        });\n        await this.customizeFonts({\n            values: values,\n            googleFonts: googleFonts,\n            googleLocalFonts: googleLocalFonts,\n            uploadedLocalFonts: uploadedLocalFonts,\n        });\n        this.config.reloadEditor();\n    }\n}\nregistry.category(\"website-plugins\").add(WebsiteFontPlugin.id, WebsiteFontPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { ClassAction } from \"@html_builder/core/core_builder_action_plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { FONT_AWESOME } from \"@html_builder/utils/option_sequence\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { BorderConfigurator } from \"@html_builder/plugins/border_configurator_option\";\n\nexport class FontAwesomeOption extends BaseOptionComponent {\n    static template = \"website.FontAwesomeOption\";\n    static selector = \"span.fa, i.fa\";\n    static exclude = \"[data-oe-xpath]\";\n    static components = { BorderConfigurator };\n}\n\nclass FontAwesomeOptionPlugin extends Plugin {\n    static id = \"fontAwesomeOptionPlugin\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(FONT_AWESOME, FontAwesomeOption)],\n        builder_actions: {\n            FaResizeAction,\n        },\n    };\n}\n\nexport class FaResizeAction extends ClassAction {\n    static id = \"faResize\";\n    apply(context) {\n        const { editingElement } = context;\n        editingElement.classList.remove(\"fa-1x\", \"fa-lg\");\n        super.apply(context);\n    }\n}\n\nregistry.category(\"website-plugins\").add(FontAwesomeOptionPlugin.id, FontAwesomeOptionPlugin);\n", "import { onWillStart, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class FormActionFieldsOption extends BaseOptionComponent {\n    static template = \"website.s_website_form_form_action_fields_option\";\n    static dependencies = [\"websiteFormOption\"];\n    static props = {\n        activeForm: { type: Object, optional: true },\n    };\n\n    setup() {\n        super.setup();\n        this.prepareFormModel = this.dependencies.websiteFormOption.prepareFormModel;\n        this.state = useState({\n            formInfo: {\n                fields: [],\n            },\n        });\n        onWillStart(this.getFormInfo.bind(this));\n        onWillUpdateProps(this.getFormInfo.bind(this));\n    }\n    async getFormInfo(props = this.props) {\n        const el = this.env.getEditingElement();\n        const formInfo = await this.prepareFormModel(el, props.activeForm);\n        Object.assign(\n            this.state.formInfo,\n            {\n                fields: [],\n                formFields: [],\n                successPage: undefined,\n            },\n            formInfo\n        );\n    }\n}\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { onWillStart, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { FormActionFieldsOption } from \"./form_action_fields_option\";\nimport { FormModelRequiredFieldAlert } from \"./form_model_required_field_alert\";\nimport {\n    getDependencyEl,\n    getFieldName,\n    getMultipleInputs,\n    isFieldCustom,\n    getCurrentFieldInputEl,\n    getModelName,\n} from \"./utils\";\nimport { formatDate, formatDateTime } from \"@web/core/l10n/dates\";\n\nconst { DateTime } = luxon;\n\nexport class FormFieldOption extends BaseOptionComponent {\n    static template = \"website.s_website_form_field_option\";\n    static dependencies = [\"websiteFormOption\"];\n    static props = {\n        redrawSequence: { type: Number, optional: true },\n    };\n    static components = { FormActionFieldsOption, FormModelRequiredFieldAlert };\n\n    setup() {\n        super.setup();\n        const { loadFieldOptionData } = this.dependencies.websiteFormOption;\n        this.state = useState({\n            availableFields: [],\n            conditionInputs: [],\n            conditionValueList: [],\n            dependencyEl: null,\n            valueList: null,\n        });\n        this.domState = useDomState((el) => {\n            const modelName = getModelName(el.closest(\"form\"));\n            const fieldName = getFieldName(el);\n            return {\n                elDataset: { ...el.dataset },\n                elClassList: [...el.classList],\n                fieldName,\n                modelName,\n                fieldTranslatedName: el.dataset.translatedName,\n            };\n        });\n        this.format = {\n            date: (value) => (value ? formatDate(DateTime.fromSeconds(parseInt(value))) : \"\"),\n            datetime: (value) =>\n                value ? formatDateTime(DateTime.fromSeconds(parseInt(value))) : \"\",\n        };\n\n        this.domStateDependency = useDomState((el) => {\n            const dependencyEl = getDependencyEl(el);\n            if (!dependencyEl) {\n                return {\n                    type: \"\",\n                    nodeName: \"\",\n                    isRecordField: false,\n                    isFormDate: false,\n                    isFormDateTime: false,\n                    hasDateTimePicker: false,\n                };\n            }\n\n            return {\n                type: dependencyEl.type,\n                nodeName: dependencyEl.nodeName,\n                isRecordField:\n                    dependencyEl.closest(\".s_website_form_field\")?.dataset.type === \"record\",\n                isFormDate: !!dependencyEl.closest(\".s_website_form_date\"),\n                isFormDateTime: !!dependencyEl.closest(\".s_website_form_datetime\"),\n                hasDateTimePicker: dependencyEl.classList.contains(\"datetimepicker-input\"),\n            };\n        });\n\n        this.domStateCurrentFieldInput = useDomState((el) => {\n            const currentFieldInputEl = getCurrentFieldInputEl(el);\n            if (!currentFieldInputEl) {\n                return {\n                    type: \"\",\n                    nodeName: \"\",\n                    isRecordField: false,\n                    isFormDate: false,\n                    isFormDateTime: false,\n                    hasDateTimePicker: false,\n                    isTextArea: false,\n                };\n            }\n            return {\n                type: currentFieldInputEl.type,\n                nodeName: currentFieldInputEl.nodeName,\n                isRecordField:\n                    currentFieldInputEl.closest(\".s_website_form_field\")?.dataset.type === \"record\",\n                isFormDate: !!currentFieldInputEl.closest(\".s_website_form_date\"),\n                isFormDateTime: !!currentFieldInputEl.closest(\".s_website_form_datetime\"),\n                hasDateTimePicker: currentFieldInputEl.classList.contains(\"datetimepicker-input\"),\n                isTextArea: currentFieldInputEl.nodeName === \"TEXTAREA\",\n            };\n        });\n\n        onWillStart(async () => {\n            const el = this.env.getEditingElement();\n            const fieldOptionData = await loadFieldOptionData(el);\n            this.state.availableFields.push(...fieldOptionData.availableFields);\n            this.state.conditionInputs.push(...fieldOptionData.conditionInputs);\n            this.state.valueList = fieldOptionData.valueList;\n            this.state.conditionValueList.push(...fieldOptionData.conditionValueList);\n        });\n        onWillUpdateProps(async (props) => {\n            const el = this.env.getEditingElement();\n            const fieldOptionData = await loadFieldOptionData(el);\n            this.state.availableFields.length = 0;\n            this.state.availableFields.push(...fieldOptionData.availableFields);\n            this.state.conditionInputs.length = 0;\n            this.state.conditionInputs.push(...fieldOptionData.conditionInputs);\n            this.state.valueList = fieldOptionData.valueList;\n            this.state.conditionValueList.length = 0;\n            this.state.conditionValueList.push(...fieldOptionData.conditionValueList);\n        });\n        // TODO select field's hack ?\n    }\n    get canHaveTextValidationCondition() {\n        return [\"text\", \"email\", \"tel\", \"url\", \"search\", \"password\", \"number\"];\n    }\n    get isTextConditionValueVisible() {\n        const el = this.env.getEditingElement();\n        const dependencyEl = getDependencyEl(el);\n        if (\n            !el.classList.contains(\"s_website_form_field_hidden_if\") ||\n            (dependencyEl &&\n                ([\"checkbox\", \"radio\"].includes(dependencyEl.type) ||\n                    dependencyEl.nodeName === \"SELECT\"))\n        ) {\n            return false;\n        }\n        if (!dependencyEl) {\n            return true;\n        }\n        if (dependencyEl?.classList.contains(\"datetimepicker-input\")) {\n            return el.dataset.visibilityComparator === \"lessyears\";\n        }\n        return (\n            ([\"text\", \"email\", \"tel\", \"url\", \"search\", \"password\", \"number\"].includes(\n                dependencyEl.type\n            ) ||\n                dependencyEl.nodeName === \"TEXTAREA\") &&\n            ![\"set\", \"!set\"].includes(el.dataset.visibilityComparator)\n        );\n    }\n    /**\n     * Determines the visibility of the text condition input field used for\n     * validation.\n     *\n     * @returns {boolean} Whether the text condition input should be visible.\n     */\n    get isTextConditionForRequirementOptionVisible() {\n        const el = this.env.getEditingElement();\n        const currentFieldInputEl = getCurrentFieldInputEl(el);\n        return (\n            el.dataset.requirementComparator &&\n            !this.domStateCurrentFieldInput.hasDateTimePicker &&\n            (this.domStateCurrentFieldInput.isTextArea ||\n                this.canHaveTextValidationCondition.includes(currentFieldInputEl.type))\n        );\n    }\n    get isTextConditionOperatorVisible() {\n        const el = this.env.getEditingElement();\n        const dependencyEl = getDependencyEl(el);\n        if (\n            !el.classList.contains(\"s_website_form_field_hidden_if\") ||\n            dependencyEl?.classList.contains(\"datetimepicker-input\")\n        ) {\n            return false;\n        }\n        return (\n            !dependencyEl ||\n            [\"text\", \"email\", \"tel\", \"url\", \"search\", \"password\"].includes(dependencyEl.type) ||\n            dependencyEl.nodeName === \"TEXTAREA\"\n        );\n    }\n    get isExistingFieldSelectType() {\n        const el = this.env.getEditingElement();\n        return !isFieldCustom(el) && [\"selection\", \"many2one\"].includes(el.dataset.type);\n    }\n    get isMultipleInputs() {\n        const el = this.env.getEditingElement();\n        return !!getMultipleInputs(el);\n    }\n    get isMaxFilesVisible() {\n        // Do not display the option if only one file is supposed to be\n        // uploaded in the field.\n        const el = this.env.getEditingElement();\n        const fieldEl = el.closest(\".s_website_form_field\");\n        return (\n            fieldEl.classList.contains(\"s_website_form_custom\") ||\n            [\"one2many\", \"many2many\"].includes(fieldEl.dataset.type)\n        );\n    }\n}\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { FormFieldOption } from \"./form_field_option\";\n\nexport class FormFieldOptionRedraw extends BaseOptionComponent {\n    static template = \"website.s_website_form_field_option_redraw\";\n    static props = FormFieldOption.props;\n    static selector = \".s_website_form_field\";\n    static exclude = \".s_website_form_dnone\";\n    static components = { FormFieldOption };\n\n    setup() {\n        super.setup();\n        this.count = 0;\n        this.domState = useDomState((el) => {\n            this.count++;\n            return {\n                redrawSequence: this.count++,\n            };\n        });\n    }\n}\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { onWillStart, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class FormModelRequiredFieldAlert extends BaseOptionComponent {\n    static template = \"website.s_website_form_model_required_field_alert\";\n    static dependencies = [\"websiteFormOption\"];\n    static props = {\n        fieldName: String,\n        modelName: String,\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            message: undefined,\n        });\n        this.fetchModels = this.dependencies.websiteFormOption.fetchModels;\n        onWillStart(async () => this.handleProps(this.props));\n        onWillUpdateProps(async (props) => this.handleProps(props));\n    }\n    async handleProps(props) {\n        // Get list of website_form compatible models, needed for alert message.\n        const el = this.env.getEditingElement();\n        const models = await this.fetchModels(el);\n        const model = models.find((model) => model.model === props.modelName);\n        const actionName = model?.website_form_label || props.modelName;\n        this.state.message = _t(\"The field \u201c%(field)s\u201d is mandatory for the action \u201c%(action)s\u201d.\", {\n            field: props.fieldName,\n            action: actionName,\n        });\n    }\n}\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { getModelName, getParsedDataFor } from \"./utils\";\nimport { FormActionFieldsOption } from \"./form_action_fields_option\";\nimport { session } from \"@web/session\";\nimport { selectElements } from \"@html_editor/utils/dom_traversal\";\n\nexport class FormOption extends BaseOptionComponent {\n    static template = \"website.s_website_form_form_option\";\n    static dependencies = [\"websiteFormOption\"];\n    static selector = \".s_website_form\";\n    static applyTo = \"form\";\n    static components = { FormActionFieldsOption };\n    static async cleanForSave(el, { dependencies, services }) {\n        for (const sigEl of el.querySelectorAll(\"input[name=website_form_signature]\")) {\n            sigEl.remove();\n        }\n\n        for (const formEl of selectElements(el, \".s_website_form form[data-model_name]\")) {\n            const model = formEl.dataset.model_name;\n            const authorizedFields = await dependencies.websiteFormOption.fetchAuthorizedFields(\n                formEl\n            );\n            const fields = [\n                ...formEl.querySelectorAll(\n                    \".s_website_form_field:not(.s_website_form_custom) .s_website_form_input\"\n                ),\n            ]\n                .map((el) => el.name)\n                .filter((name) => !authorizedFields[name]?._property);\n            if (fields.length) {\n                services.orm.call(\"ir.model.fields\", \"formbuilder_whitelist\", [\n                    model,\n                    [...new Set(fields)],\n                ]);\n            }\n        }\n    }\n\n    setup() {\n        super.setup();\n        const { prepareFormModel, applyFormModel, fetchModels } =\n            this.dependencies.websiteFormOption;\n        this.hasRecaptchaKey = !!session.recaptcha_public_key;\n\n        // Get potential message\n        const el = this.env.getEditingElement();\n        this.messageEl = el.parentElement.querySelector(\".s_website_form_end_message\");\n        this.showEndMessage = false;\n        // Get the email_to value from the data-for attribute if it exists. We\n        // use it if there is no value on the email_to input.\n        const formId = el.id;\n        const dataForValues = getParsedDataFor(formId, el.ownerDocument);\n        if (dataForValues) {\n            this.dataForEmailTo = dataForValues[\"email_to\"];\n        }\n        this.state = useDomState(async (el) => {\n            const modelName = getModelName(el);\n\n            // Hide change form parameters option for forms e.g. User should not\n            // be enable to change existing job application form to opportunity\n            // form in 'Apply job' page.\n            this.modelCantChange = !!el.getAttribute(\"hide-change-model\");\n\n            // Get list of website_form compatible models.\n            const models = await fetchModels(el);\n            const activeForm = models.find((m) => m.model === modelName);\n\n            // If the form has no model it means a new snippet has been dropped.\n            // Apply the default model selected in willStart on it.\n            if (!el.dataset.model_name) {\n                const formInfo = await prepareFormModel(el, activeForm);\n                applyFormModel(el, activeForm, activeForm.id, formInfo);\n            }\n            return {\n                models,\n                activeForm,\n            };\n        });\n    }\n}\n", "import { useOperation } from \"@html_builder/core/operation_plugin\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class FormOptionAddFieldButton extends BaseOptionComponent {\n    static template = \"website.s_website_form_form_option_add_field_button\";\n    static props = {\n        addField: Function,\n        tooltip: String,\n    };\n\n    setup() {\n        this.callOperation = useOperation();\n    }\n\n    addField() {\n        this.callOperation(() => {\n            this.props.addField(this.env.getEditingElement());\n        });\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { Cache } from \"@web/core/utils/cache\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { reactive } from \"@odoo/owl\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { redirect } from \"@web/core/utils/urls\";\nimport { FormFieldOptionRedraw } from \"./form_field_option_redraw\";\nimport { FormOptionAddFieldButton } from \"./form_option_add_field_button\";\nimport {\n    deleteConditionalVisibility,\n    findCircular,\n    getActiveField,\n    getCustomField,\n    getDefaultFormat,\n    getDependencyEl,\n    getDomain,\n    getFieldFormat,\n    getFieldName,\n    getFieldType,\n    getLabelPosition,\n    getMark,\n    getModelName,\n    getMultipleInputs,\n    getNewRecordId,\n    getQuotesEncodedName,\n    getSelect,\n    isFieldCustom,\n    isOptionalMark,\n    isRequiredMark,\n    renderField,\n    replaceFieldElement,\n    setActiveProperties,\n    setVisibilityDependency,\n    getParsedDataFor,\n    rerenderField,\n} from \"./utils\";\nimport { SyncCache } from \"@html_builder/utils/sync_cache\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { FormOption } from \"./form_option\";\nimport { isSmallInteger } from \"@html_builder/utils/utils\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { formatDate } from \"@web/core/l10n/dates\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\n/**\n * @typedef { Object } FormOptionShared\n * @property { FormOptionPlugin['prepareFormModel'] } prepareFormModel\n * @property { FormOptionPlugin['getModelsCache'] } getModelsCache\n * @property { FormOptionPlugin['applyFormModel'] } applyFormModel\n * @property { FormOptionPlugin['addHiddenField'] } addHiddenField\n * @property { FormOptionPlugin['fetchAuthorizedFields'] } fetchAuthorizedFields\n * @property { FormOptionPlugin['loadFieldOptionData'] } loadFieldOptionData\n * @property { FormOptionPlugin['prepareFields'] } prepareFields\n * @property { FormOptionPlugin['replaceField'] } replaceField\n * @property { FormOptionPlugin['prepareConditionInputs'] } prepareConditionInputs\n * @property { FormOptionPlugin['setLabelsMark'] } setLabelsMark\n * @property { FormOptionPlugin['clearValidationDataset'] } clearValidationDataset\n * @property { FormOptionPlugin['defaultMessage'] } defaultMessage\n * @property { FormOptionPlugin['fetchModels'] } fetchModels\n */\n\nconst { DateTime } = luxon;\n\nexport class WebsiteFormSubmitOption extends BaseOptionComponent {\n    static template = \"website.s_website_form_submit_option\";\n    static selector = \".s_website_form_submit\";\n    static exclude = \".s_website_form_no_submit_options\";\n}\n\nconst DEFAULT_EMAIL_TO_VALUE = \"info@yourcompany.example.com\";\nexport class FormOptionPlugin extends Plugin {\n    static id = \"websiteFormOption\";\n    static dependencies = [\"builderActions\", \"builderOptions\", \"savePlugin\"];\n    static shared = [\n        \"prepareFormModel\",\n        \"getModelsCache\",\n        \"applyFormModel\",\n        \"addHiddenField\",\n        \"fetchAuthorizedFields\",\n        \"loadFieldOptionData\",\n        \"prepareFields\",\n        \"replaceField\",\n        \"prepareConditionInputs\",\n        \"setLabelsMark\",\n        \"clearValidationDataset\",\n        \"defaultMessage\",\n        \"fetchModels\",\n    ];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_header_middle_buttons: [\n            {\n                Component: FormOptionAddFieldButton,\n                selector: \".s_website_form\",\n                applyTo: \"form\",\n                props: {\n                    addField: (formEl) => this.addFieldToForm(formEl),\n                    tooltip: _t(\"Add a new field at the end\"),\n                },\n            },\n            {\n                Component: FormOptionAddFieldButton,\n                selector: \".s_website_form_field\",\n                exclude: \".s_website_form_dnone\",\n                props: {\n                    addField: (fieldEl) => this.addFieldAfterField(fieldEl),\n                    tooltip: _t(\"Add a new field after this one\"),\n                },\n            },\n        ],\n        clone_disabled_reason_providers: ({ el, reasons }) => {\n            if (\n                el.classList.contains(\"s_website_form_field\") &&\n                !el.classList.contains(\"s_website_form_custom\")\n            ) {\n                reasons.push(_t(\"You cannot duplicate this field.\"));\n            }\n            if (el.classList.contains(\"s_website_form_submit\")) {\n                reasons.push(_t(\"You can't duplicate the submit button of the form.\"));\n            }\n        },\n        remove_disabled_reason_providers: ({ el, reasons }) => {\n            if (el.classList.contains(\"s_website_form_model_required\")) {\n                reasons.push(\n                    _t(\n                        \"This field is mandatory for this action. You cannot remove it. Try hiding it with the 'Visibility' option instead and add it a default value.\"\n                    )\n                );\n            }\n            if (el.classList.contains(\"s_website_form_submit\")) {\n                reasons.push(_t(\"You can't remove the submit button of the form\"));\n            }\n        },\n        builder_options: [FormOption, FormFieldOptionRedraw, WebsiteFormSubmitOption],\n        builder_actions: {\n            // Form actions\n            // Components that use this action MUST await fetchModels before they start.\n            SelectAction,\n            // Select the value of a field (hidden) that will be used on the model as a preset.\n            // ie: The Job you apply for if the form is on that job's page.\n            AddActionFieldAction,\n            PromptSaveRedirectAction,\n            UpdateLabelsMarkAction,\n            SetMarkAction,\n            OnSuccessAction,\n            ToggleEndMessageAction,\n            FormToggleRecaptchaLegalAction,\n            // Field actions\n            CustomFieldAction,\n            ExistingFieldAction,\n            SelectTypeAction,\n            ExistingFieldSelectTypeAction,\n            MultiCheckboxDisplayAction,\n            SetLabelTextAction,\n            SelectLabelPositionAction,\n            ToggleDescriptionAction,\n            SelectTextareaValueAction,\n            ToggleRequiredAction,\n            SetVisibilityAction,\n            SetVisibilityDependencyAction,\n            SetFormCustomFieldValueListAction,\n            PropertyAction,\n            SetCustomErrorMessageAction,\n            SetDefaultErrorMessageAction,\n            SetRequirementComparatorAction,\n            SetMultipleFilesAction,\n        },\n        content_not_editable_selectors: \".s_website_form form\",\n        content_editable_selectors: [\n            \".s_website_form_send\",\n            \".s_website_form_field_description\",\n            \".s_website_form_recaptcha\",\n            \".row > div:not(.s_website_form_field, .s_website_form_submit, .s_website_form_field *, .s_website_form_submit *)\",\n        ].map((selector) => `.s_website_form form ${selector}`),\n        clean_for_save_handlers: ({ root: rootEl }) => {\n            this.removeSuccessMessagePreviews(rootEl);\n        },\n        dropzone_selector: [\n            {\n                selector: \".s_website_form\",\n                excludeAncestor: \"form\",\n            },\n            {\n                selector: \".s_website_form_field, .s_website_form_submit\",\n                exclude: \".s_website_form_dnone\",\n                dropNear: \".s_website_form_field\",\n                dropLockWithin: \"form\",\n            },\n        ],\n        so_content_addition_selector: [\".s_website_form\"],\n        on_snippet_dropped_handlers: this.onSnippetDropped.bind(this),\n        on_cloned_handlers: this.onCloned.bind(this),\n    };\n    setup() {\n        this.modelsCache = new SyncCache(this._fetchModels.bind(this));\n        this.fieldRecordsCache = new SyncCache(this._fetchFieldRecords.bind(this));\n        this.authorizedFieldsCache = new Cache(\n            this._fetchAuthorizedFields.bind(this),\n            ({ cacheKey }) => cacheKey\n        );\n        this.visibilityConditionCachedRecords = new Cache(\n            this._getVisibilityConditionCachedRecords.bind(this),\n            JSON.stringify\n        );\n    }\n    destroy() {\n        super.destroy();\n        this.modelsCache.invalidate();\n        this.fieldRecordsCache.invalidate();\n        this.authorizedFieldsCache.invalidate();\n        this.visibilityConditionCachedRecords.invalidate();\n    }\n    getModelsCache(formEl) {\n        // Through a method so that it can be overridden.\n        return this.modelsCache.get();\n    }\n    async fetchModels(formEl) {\n        return this.modelsCache.preload();\n    }\n    async _fetchModels() {\n        return await this.services.orm.call(\"ir.model\", \"get_compatible_form_models\");\n    }\n    async fetchFieldRecords(field) {\n        if (field) {\n            field.records = await this.fieldRecordsCache.preload(field);\n            return field.records;\n        }\n    }\n    /**\n     * Returns a promise which is resolved once the records of the field\n     * have been retrieved.\n     *\n     * @param {Object} field\n     * @returns {Promise<Object>}\n     */\n    async _fetchFieldRecords(field) {\n        // TODO remove this - put there to avoid crash\n        if (!field) {\n            return;\n        }\n        // Convert the required boolean to a value directly usable\n        // in qweb js to avoid duplicating this in the templates\n        field.required = field.required ? 1 : null;\n\n        if (field.records) {\n            return field.records;\n        }\n        if (field._property && field.type === \"tags\") {\n            // Convert tags to records to avoid added complexity.\n            // Tag ids need to escape \",\" to be able to recover their value on\n            // the server side if they contain \",\".\n            field.records = field.tags.map((tag) => ({\n                id: tag[0].replaceAll(\"\\\\\", \"\\\\/\").replaceAll(\",\", \"\\\\,\"),\n                display_name: tag[1],\n            }));\n        } else if (field._property && field.comodel) {\n            field.records = await this.services.orm.searchRead(field.comodel, field.domain || [], [\n                \"display_name\",\n            ]);\n        } else if (field.type === \"selection\") {\n            // Set selection as records to avoid added complexity.\n            field.records = field.selection.map((el) => ({\n                id: el[0],\n                display_name: el[1],\n            }));\n        } else if (field.relation && field.relation !== \"ir.attachment\") {\n            const fieldNames = field.fieldName ? [field.fieldName] : [\"display_name\"];\n            field.records = await this.services.orm.searchRead(\n                field.relation,\n                field.domain || [],\n                fieldNames\n            );\n            if (field.fieldName) {\n                field.records.forEach((r) => (r[\"display_name\"] = r[field.fieldName]));\n            }\n        }\n        return field.records;\n    }\n    async prepareFormModel(el, activeForm) {\n        const formKey = activeForm?.website_form_key;\n        const formInfo = registry.category(\"website.form_editor_actions\").get(formKey, null);\n        if (formInfo) {\n            const formatInfo = getDefaultFormat(el);\n            await Promise.all(\n                formInfo.formFields.map((field) => {\n                    field.formatInfo = formatInfo;\n                    return this.fetchFieldRecords(field);\n                })\n            );\n            await this.fetchFormInfoFields(formInfo);\n        }\n        return formInfo;\n    }\n    /**\n     * Add a hidden field to the form\n     *\n     * @param {HTMLElement} el\n     * @param {string} value\n     * @param {string} fieldName\n     */\n    addHiddenField(el, value, fieldName) {\n        for (const hiddenEl of el.querySelectorAll(\n            `.s_website_form_dnone:has(input[name=\"${fieldName}\"])`\n        )) {\n            hiddenEl.remove();\n        }\n        // For the email_to field, we keep the field even if it has no value so\n        // that the email is sent to data-for value or to the default email.\n        if (fieldName === \"email_to\" && !value && !this.dataForEmailTo) {\n            value = DEFAULT_EMAIL_TO_VALUE;\n        }\n        if (value || fieldName === \"email_to\") {\n            const hiddenField = renderToElement(\"website.form_field_hidden\", {\n                field: {\n                    name: fieldName,\n                    value: value,\n                    dnone: true,\n                    formatInfo: {},\n                },\n            });\n            el.querySelector(\".s_website_form_submit\").insertAdjacentElement(\n                \"beforebegin\",\n                hiddenField\n            );\n        }\n    }\n    /**\n     * Apply the model on the form changing its fields\n     *\n     * @param {HTMLElement} el\n     * @param {Object} activeForm\n     * @param {Integer} modelId\n     * @param {Object} formInfo obtained from prepareFormModel\n     */\n    applyFormModel(el, activeForm, modelId, formInfo) {\n        let oldFormInfo;\n        if (modelId) {\n            const oldFormKey = activeForm.website_form_key;\n            if (oldFormKey) {\n                oldFormInfo = registry\n                    .category(\"website.form_editor_actions\")\n                    .get(oldFormKey, null);\n            }\n            for (const fieldEl of el.querySelectorAll(\".s_website_form_field\")) {\n                fieldEl.remove();\n            }\n            activeForm = this.getModelsCache(el).find((model) => model.id === modelId);\n        }\n        // Success page\n        if (!el.dataset.successMode) {\n            el.dataset.successMode = \"redirect\";\n        }\n        if (el.dataset.successMode === \"redirect\") {\n            const currentSuccessPage = el.dataset.successPage;\n            if (formInfo && formInfo.successPage) {\n                el.dataset.successPage = formInfo.successPage;\n            } else if (\n                !oldFormInfo ||\n                (oldFormInfo !== formInfo &&\n                    oldFormInfo.successPage &&\n                    currentSuccessPage === oldFormInfo.successPage)\n            ) {\n                el.dataset.successPage = \"/contactus-thank-you\";\n            }\n        }\n        // Model name\n        el.dataset.model_name = activeForm.model;\n        // Load template\n        if (formInfo) {\n            const formatInfo = getDefaultFormat(el);\n            formInfo.formFields.forEach((field) => {\n                // Create a shallow copy of field to prevent unintended\n                // mutations to the original field stored in the registry\n                const _field = { ...field };\n                _field.formatInfo = formatInfo;\n                const locationEl = el.querySelector(\n                    \".s_website_form_submit, .s_website_form_recaptcha\"\n                );\n                locationEl.insertAdjacentElement(\"beforebegin\", renderField(_field));\n            });\n            // Special case: handle hidden fields separately.\n            // In some forms (e.g., contact forms), the \"email_to\" field must be included as hidden.\n            // For example, this may force the 'email_to' value to a dummy/default one on the\n            // contact us form just by interacting with it.\n            formInfo.fields?.forEach(field => {\n                if (field.defaultValue) {\n                    this.addHiddenField(el, field.defaultValue, field.name);\n                }\n            });\n        }\n    }\n    /**\n     * Ensures formInfo fields are fetched.\n     */\n    async fetchFormInfoFields(formInfo) {\n        if (formInfo.fields) {\n            const proms = formInfo.fields.map((field) => this.fetchFieldRecords(field));\n            await Promise.all(proms);\n        }\n    }\n    async fetchAuthorizedFields(formEl) {\n        // Combine model and fields into cache key.\n        const model = getModelName(formEl);\n        const propertyOrigins = {};\n        const parts = [model];\n        for (const hiddenInputEl of [...formEl.querySelectorAll(\"input[type=hidden]\")].sort(\n            (firstEl, secondEl) => firstEl.name.localeCompare(secondEl.name)\n        )) {\n            // Pushing using the name order to avoid being impacted by the\n            // order of hidden fields within the DOM.\n            parts.push(hiddenInputEl.name);\n            parts.push(hiddenInputEl.value);\n            propertyOrigins[hiddenInputEl.name] = hiddenInputEl.value;\n        }\n        const cacheKey = parts.join(\"/\");\n        return this.authorizedFieldsCache.read({ cacheKey, model, propertyOrigins });\n    }\n    async _fetchAuthorizedFields({ cacheKey, model, propertyOrigins }) {\n        return this.services.orm.call(\"ir.model\", \"get_authorized_fields\", [\n            model,\n            propertyOrigins,\n        ]);\n    }\n    async _getVisibilityConditionCachedRecords(model, domain, fields, kwargs = {}) {\n        return this.services.orm.searchRead(model, domain, fields, {\n            ...kwargs,\n            limit: 1000, // Safeguard to not crash DBs\n        });\n    }\n\n    /**\n     * Set the correct mark on all fields.\n     */\n    setLabelsMark(formEl) {\n        formEl.querySelectorAll(\".s_website_form_mark\").forEach((el) => el.remove());\n        const mark = getMark(formEl);\n        if (!mark) {\n            return;\n        }\n        let fieldsToMark = [];\n        const requiredSelector = \".s_website_form_model_required, .s_website_form_required\";\n        const fields = Array.from(formEl.querySelectorAll(\".s_website_form_field\"));\n        if (isRequiredMark(formEl)) {\n            fieldsToMark = fields.filter((el) => el.matches(requiredSelector));\n        } else if (isOptionalMark(formEl)) {\n            fieldsToMark = fields.filter((el) => !el.matches(requiredSelector));\n        }\n        fieldsToMark.forEach((field) => {\n            const span = document.createElement(\"span\");\n            span.classList.add(\"s_website_form_mark\");\n            span.textContent = ` ${mark}`;\n            field.querySelector(\".s_website_form_label\").appendChild(span);\n        });\n    }\n    addFieldToForm(formEl) {\n        const field = getCustomField(\"char\", _t(\"Custom Text\"));\n        field.formatInfo = getDefaultFormat(formEl);\n        const fieldEl = renderField(field);\n        let locationEl = formEl.querySelector(\n            \".s_website_form_submit, .s_website_form_recaptcha\"\n        );\n        if (!locationEl) {\n            locationEl = formEl.querySelector(\".s_website_form_rows\");\n            locationEl.insertAdjacentElement(\"beforeend\", fieldEl);\n        } else {\n            locationEl.insertAdjacentElement(\"beforebegin\", fieldEl);\n        }\n        this.dependencies.builderOptions.setNextTarget(fieldEl);\n    }\n    addFieldAfterField(fieldEl) {\n        const formEl = fieldEl.closest(\"form\");\n        const field = getCustomField(\"char\", _t(\"Custom Text\"));\n        field.formatInfo = getFieldFormat(fieldEl);\n        field.formatInfo.requiredMark = isRequiredMark(formEl);\n        field.formatInfo.optionalMark = isOptionalMark(formEl);\n        field.formatInfo.mark = getMark(formEl);\n        const newFieldEl = renderField(field);\n        fieldEl.insertAdjacentElement(\"afterend\", newFieldEl);\n        this.dependencies.builderOptions.setNextTarget(newFieldEl);\n    }\n    /**\n     * To be used in load for any action that uses getActiveField or\n     * replaceField\n     */\n    async prepareFields({ editingElement: fieldEl, value }) {\n        // TODO Through cache ?\n        const fieldOptionData = await this.loadFieldOptionData(fieldEl);\n        const fieldName = getFieldName(fieldEl);\n        const field = fieldOptionData.fields[fieldName];\n        await this.fetchFieldRecords(field);\n        if (fieldOptionData.fields[value]) {\n            await this.fetchFieldRecords(fieldOptionData.fields[value]);\n        }\n        return fieldOptionData.fields;\n    }\n    async prepareConditionInputs({ editingElement: fieldEl, value }) {\n        // TODO Through cache ?\n        const fieldOptionData = await this.loadFieldOptionData(fieldEl);\n        const fieldName = getFieldName(fieldEl);\n        const field = fieldOptionData.fields[fieldName];\n        await this.fetchFieldRecords(field);\n        if (fieldOptionData.fields[value]) {\n            await this.fetchFieldRecords(fieldOptionData.fields[value]);\n        }\n        return fieldOptionData.conditionInputs;\n    }\n    /**\n     * Replaces the old field content with the field provided.\n     *\n     * @param {HTMLElement} oldFieldEl\n     * @param {Object} field\n     * @param {Array} fields\n     * @returns {Promise}\n     */\n    replaceField(oldFieldEl, field, fields) {\n        const activeField = getActiveField(oldFieldEl, { fields });\n        if (activeField.type !== field.type) {\n            field.value = \"\";\n        }\n        const targetEl = oldFieldEl.querySelector(\".s_website_form_input\");\n        if (targetEl) {\n            if ([\"checkbox\", \"radio\"].includes(targetEl.getAttribute(\"type\"))) {\n                // Remove first checkbox/radio's id's final '0'.\n                field.id = targetEl.id.slice(0, -1);\n            } else {\n                field.id = targetEl.id;\n            }\n        }\n\n        // Synchronize the possible values with the fields whose visibility\n        // depends on the current field\n        const newValuesText = field.records ? field.records.map((record) => record.id) : [];\n        const inputEls = oldFieldEl.querySelectorAll(\".s_website_form_input, option\");\n        const inputName = oldFieldEl.querySelector(\".s_website_form_input\")?.name;\n        const formEl = oldFieldEl.closest(\".s_website_form\");\n        for (let i = 0; i < inputEls.length; i++) {\n            const input = inputEls[i];\n            if (newValuesText[i] && input.value && !newValuesText.includes(input.value)) {\n                for (const dependentEl of formEl.querySelectorAll(\n                    `[data-visibility-condition=\"${CSS.escape(\n                        input.value\n                    )}\"][data-visibility-dependency=\"${CSS.escape(inputName)}\"]`\n                )) {\n                    dependentEl.dataset.visibilityCondition = newValuesText[i];\n                }\n                break;\n            }\n        }\n\n        const fieldEl = renderField(field);\n        replaceFieldElement(oldFieldEl, fieldEl);\n    }\n    async loadFieldOptionData(fieldEl) {\n        const formEl = fieldEl.closest(\"form\");\n        const fields = {};\n        // Get the authorized existing fields for the form model\n        // Do it on each render because of custom property fields which can\n        // change depending on the project selected.\n        const existingFields = await this.fetchAuthorizedFields(formEl).then((fieldsFromCache) => {\n            for (const [fieldName, field] of Object.entries(fieldsFromCache)) {\n                field.name = fieldName;\n                const fieldDomain = getDomain(formEl, field.name, field.type, field.relation);\n                field.domain = fieldDomain || field.domain || [];\n                fields[fieldName] = field;\n            }\n            return Object.keys(fieldsFromCache)\n                .map((key) => {\n                    const field = fieldsFromCache[key];\n                    return {\n                        name: field.name,\n                        string: field.string,\n                    };\n                })\n                .sort((a, b) =>\n                    a.string.localeCompare(b.string, undefined, {\n                        numeric: true,\n                        sensitivity: \"base\",\n                    })\n                );\n        });\n        // Update available visibility dependencies\n        const existingDependencyNames = [];\n        const conditionInputs = [];\n        for (const el of formEl.querySelectorAll(\n            \".s_website_form_field:not(.s_website_form_dnone), .s_website_form_field[data-type]\"\n        )) {\n            const inputEl = el.querySelector(\".s_website_form_input\");\n            if (\n                el.querySelector(\".s_website_form_label_content\") &&\n                inputEl &&\n                inputEl.name &&\n                inputEl.name !== fieldEl.querySelector(\".s_website_form_input\").name &&\n                !existingDependencyNames.includes(inputEl.name) &&\n                !findCircular(el, fieldEl)\n            ) {\n                conditionInputs.push({\n                    name: inputEl.name,\n                    textContent: el.querySelector(\".s_website_form_label_content\").textContent,\n                });\n                existingDependencyNames.push(inputEl.name);\n            }\n        }\n\n        const comparator = fieldEl.dataset.visibilityComparator;\n        const dependencyEl = getDependencyEl(fieldEl);\n        const conditionValueList = [];\n        if (dependencyEl) {\n            const containerEl = dependencyEl.closest(\".s_website_form_field\");\n            const fieldType = containerEl?.dataset.type;\n            if (\n                [\"radio\", \"checkbox\"].includes(dependencyEl.type) ||\n                dependencyEl.nodeName === \"SELECT\" ||\n                fieldType === \"record\"\n            ) {\n                // Update available visibility options\n                const inputContainerEl = fieldEl;\n                if (dependencyEl.nodeName === \"SELECT\") {\n                    for (const option of dependencyEl.querySelectorAll(\"option\")) {\n                        conditionValueList.push({\n                            value: option.value,\n                            textContent: option.textContent || `<${_t(\"no value\")}>`,\n                        });\n                    }\n                    if (!inputContainerEl.dataset.visibilityCondition) {\n                        inputContainerEl.dataset.visibilityCondition =\n                            dependencyEl.querySelector(\"option\").value;\n                    }\n                } else if (fieldType === \"record\") {\n                    const model = containerEl.dataset.model;\n                    const idField = containerEl.dataset.idField || \"id\";\n                    const displayNameField = containerEl.dataset.displayNameField || \"display_name\";\n                    const records = await this.visibilityConditionCachedRecords.read(\n                        model,\n                        [],\n                        [idField, displayNameField]\n                    );\n                    for (const record of records) {\n                        conditionValueList.push({\n                            value: String(record[idField]),\n                            textContent: record[displayNameField],\n                        });\n                    }\n                    if (!inputContainerEl.dataset.visibilityCondition) {\n                        inputContainerEl.dataset.visibilityCondition = String(\n                            records[0]?.[idField]\n                        );\n                    }\n                } else {\n                    // DependencyEl is a radio or a checkbox\n                    const dependencyContainerEl = dependencyEl.closest(\".s_website_form_field\");\n                    const inputsInDependencyContainer =\n                        dependencyContainerEl.querySelectorAll(\".s_website_form_input\");\n                    // TODO: @owl-options already wrong in master for e.g. Project/Tags\n                    for (const el of inputsInDependencyContainer) {\n                        conditionValueList.push({\n                            value: el.value,\n                            textContent:\n                                inputsInDependencyContainer.length === 1\n                                    ? el.value\n                                    : dependencyContainerEl.querySelector(`label[for=\"${el.id}\"]`)\n                                          .textContent,\n                        });\n                    }\n                    if (!inputContainerEl.dataset.visibilityCondition) {\n                        inputContainerEl.dataset.visibilityCondition =\n                            inputsInDependencyContainer[0].value;\n                    }\n                }\n                if (!inputContainerEl.dataset.visibilityComparator) {\n                    inputContainerEl.dataset.visibilityComparator = \"selected\";\n                }\n            }\n            if (!comparator) {\n                // Set a default comparator according to the type of dependency\n                if (dependencyEl.dataset.target) {\n                    fieldEl.dataset.visibilityComparator = \"after\";\n                } else if (\n                    [\"text\", \"email\", \"tel\", \"url\", \"search\", \"password\", \"number\"].includes(\n                        dependencyEl.type\n                    ) ||\n                    dependencyEl.nodeName === \"TEXTAREA\"\n                ) {\n                    fieldEl.dataset.visibilityComparator = \"equal\";\n                } else if (dependencyEl.type === \"file\") {\n                    fieldEl.dataset.visibilityComparator = \"fileSet\";\n                }\n            }\n        }\n\n        const currentFieldName = getFieldName(fieldEl);\n        const fieldsInForm = Array.from(\n            formEl.querySelectorAll(\n                \".s_website_form_field:not(.s_website_form_custom) .s_website_form_input\"\n            )\n        )\n            .map((el) => el.name)\n            .filter((el) => el !== currentFieldName);\n        const availableFields = existingFields.filter(\n            (field) => !fieldsInForm.includes(field.name)\n        );\n\n        const selectEl = getSelect(fieldEl);\n        const multipleInputsEl = getMultipleInputs(fieldEl);\n        let valueList = undefined;\n        if (selectEl || multipleInputsEl) {\n            const field = Object.assign({}, fields[getFieldName(fieldEl)]);\n            const type = getFieldType(fieldEl);\n\n            const [optionText, checkType] = selectEl\n                ? [_t(\"Option List\"), \"exclusive_boolean\"]\n                : type === \"selection\"\n                ? [_t(\"Radio Button List\"), \"exclusive_boolean\"]\n                : [_t(\"Checkbox List\"), \"boolean\"];\n            const defaults = [...fieldEl.querySelectorAll(\"[checked], [selected]\")].map((el) =>\n                isSmallInteger(el.value) ? parseInt(el.value) : el.value\n            );\n            let availableRecords = undefined;\n            if (!isFieldCustom(fieldEl)) {\n                await this.fetchFieldRecords(field);\n                availableRecords = JSON.stringify(field.records);\n            }\n            valueList = reactive({\n                title: optionText,\n                addItemTitle: _t(\"Add New Option\"),\n                checkType,\n                defaultItemName: _t(\"Item\"),\n                hasDefault: [\"one2many\", \"many2many\"].includes(type) ? \"multiple\" : \"unique\",\n                defaults: JSON.stringify(defaults),\n                availableRecords: availableRecords,\n                newRecordId: isFieldCustom(fieldEl) ? getNewRecordId(fieldEl) : \"\",\n            });\n        }\n        return {\n            fields,\n            existingFields,\n            conditionInputs,\n            availableFields,\n            valueList,\n            conditionValueList,\n        };\n    }\n    /**\n     * Handler called when a snippet is dropped.\n     *\n     * @param {Object} params\n     * @param {HTMLElement} params.snippetEl - The dropped snippet element.\n     */\n    async onSnippetDropped({ snippetEl }) {\n        // Re-render the fields to ensure each field gets a unique ID.\n        await this.rerenderFieldsInElement(snippetEl);\n    }\n    /**\n     * Handler called when an element is cloned.\n     *\n     * @param {Object} params\n     * @param {HTMLElement} params.cloneEl - The cloned element.\n     */\n    async onCloned({ cloneEl }) {\n        // Re-render the fields to ensure each field gets a unique ID.\n        await this.rerenderFieldsInElement(cloneEl);\n\n        this.removeSuccessMessagePreviews(cloneEl);\n    }\n    /**\n     * Re-renders all valid fields inside the given element to ensure\n     * each field gets a unique ID.\n     *\n     * Handles:\n     * - A single field element\n     * - A form element\n     * - Any container that may include one or more forms\n     *\n     * @param {HTMLElement} rootEl\n     */\n    async rerenderFieldsInElement(rootEl) {\n        if (rootEl.matches(\"[data-name='Field']:not(.s_website_form_dnone)\")) {\n            // The root element is a single field - rerender it directly\n            const { fields } = await this.loadFieldOptionData(rootEl);\n            rerenderField(rootEl, fields);\n        } else {\n            // The root element may be a form or contain multiple forms -\n            // rerender them all\n            for (const formEl of selectElements(rootEl, \".s_website_form\")) {\n                const formFieldsToRerender = formEl.querySelectorAll(\n                    \"[data-name='Field']:not(.s_website_form_dnone)\"\n                );\n                if (formFieldsToRerender.length === 0) {\n                    continue;\n                }\n                const { fields } = await this.loadFieldOptionData(formFieldsToRerender[0]);\n                for (const fieldEl of formFieldsToRerender) {\n                    rerenderField(fieldEl, fields);\n                }\n            }\n        }\n    }\n    /**\n     * Removes all the success form message previews that are in the given root\n     * element.\n     *\n     * @param {HTMLElement} rootEl\n     */\n    removeSuccessMessagePreviews(rootEl) {\n        const toCleanEls = rootEl.querySelectorAll(\".o_show_form_success_message\");\n        toCleanEls.forEach((el) => el.classList.remove(\"o_show_form_success_message\"));\n    }\n    /**\n     * Clear the dataset of the field to avoid keeping old values.\n     *\n     * @params {HTMLElement} fieldEl - The field element to clear.\n     */\n    clearValidationDataset(fieldEl) {\n        delete fieldEl.dataset.customError;\n        delete fieldEl.dataset.errorMessage;\n        delete fieldEl.dataset.requirementBetween;\n        delete fieldEl.dataset.requirementCondition;\n    }\n\n    /**\n     * Generates an error message for requirement set on field if validation fails.\n     *\n     * @param {string} [comparator] The method used to form the error message.\n     * @param {string} [condition] The expected value of the field.\n     * @param {string} [between] The maximum date value if the comparator is\n     *      'between' or '!between'.\n     * @returns {string} The default error message.\n     */\n    defaultMessage(comparator, condition, between, type) {\n        const textMessages = {\n            contains: _t(\"This field must include keyword %s.\", condition),\n            \"!contains\": _t(\"This field must not include keyword %s.\", condition),\n            substring: _t(\"This field must include keyword %s.\", condition),\n            \"!substring\": _t(\"This field must not include keyword %s.\", condition),\n            greater: _t(\"Invalid: field is not greater than %s.\", condition),\n            less: _t(\"Invalid: field is not less than %s.\", condition),\n            \"greater or equal\": _t(\"Invalid: field is not greater than or equal to %s.\", condition),\n            \"less or equal\": _t(\"Invalid: field is not less than or equal to %s.\", condition),\n        };\n\n        if (condition && textMessages[comparator]) {\n            return textMessages[comparator];\n        }\n\n        if ([\"date\", \"datetime\"].includes(type)) {\n            const format = type === \"date\" ? localization.dateFormat : localization.dateTimeFormat;\n            const start = formatDate(DateTime.fromSeconds(parseInt(condition)), { format });\n            const end = formatDate(DateTime.fromSeconds(parseInt(between)), { format });\n\n            const dateMessages = {\n                dateEqual: _t(\n                    \"Entered date or time is not correct! It must be %(start)s (%(format)s).\",\n                    { start, format }\n                ),\n                \"date!equal\": _t(\n                    \"Entered date or time is not correct! It must not be %(start)s (%(format)s).\",\n                    { start, format }\n                ),\n                before: _t(\n                    \"Entered date or time is not correct! It must be before %(start)s (%(format)s).\",\n                    { start, format }\n                ),\n                after: _t(\n                    \"Entered date or time is not correct! It must be after %(start)s (%(format)s).\",\n                    { start, format }\n                ),\n                \"equal or before\": _t(\n                    \"Entered date or time is not correct! It must be before or equal to %(start)s (%(format)s).\",\n                    { start, format }\n                ),\n                \"equal or after\": _t(\n                    \"Entered date or time is not correct! It must be after or equal to %(start)s (%(format)s).\",\n                    { start, format }\n                ),\n                between: _t(\n                    \"Entered date or time is not correct! It must be within %(start)s and %(end)s (%(format)s).\",\n                    { start, end, format }\n                ),\n                \"!between\": _t(\n                    \"Entered date or time is not correct! It must not be within %(start)s and %(end)s (%(format)s).\",\n                    { start, end, format }\n                ),\n            };\n\n            if (condition && dateMessages[comparator]) {\n                return dateMessages[comparator];\n            }\n        }\n\n        return _t(\"An error has occurred, the form has not been sent.\");\n    }\n}\n\n// Form actions\n// Components that use this action MUST await fetchModels before they start.\nexport class SelectAction extends BuilderAction {\n    static id = \"selectAction\";\n    static dependencies = [\"websiteFormOption\"];\n    async load({ editingElement: el, value: modelId }) {\n        const modelCantChange = !!el.getAttribute(\"hide-change-model\");\n        if (modelCantChange) {\n            return;\n        }\n        const activeForm = this.dependencies.websiteFormOption\n            .getModelsCache(el)\n            .find((model) => model.id === parseInt(modelId));\n        return {\n            formInfo: await this.dependencies.websiteFormOption.prepareFormModel(el, activeForm),\n        };\n    }\n    apply({ editingElement: el, value: modelId, loadResult }) {\n        if (!loadResult) {\n            return;\n        }\n        const models = this.dependencies.websiteFormOption.getModelsCache(el);\n        const targetModelName = getModelName(el);\n        const activeForm = models.find((m) => m.model === targetModelName);\n        this.dependencies.websiteFormOption.applyFormModel(\n            el,\n            activeForm,\n            parseInt(modelId),\n            loadResult.formInfo\n        );\n    }\n    isApplied({ editingElement: el, value: modelId }) {\n        const models = this.dependencies.websiteFormOption.getModelsCache(el);\n        const targetModelName = getModelName(el);\n        const activeForm = models.find((m) => m.model === targetModelName);\n        return parseInt(modelId) === activeForm.id;\n    }\n}\n// Select the value of a field (hidden) that will be used on the model as a preset.\n// ie: The Job you apply for if the form is on that job's page.\nexport class AddActionFieldAction extends BuilderAction {\n    static id = \"addActionField\";\n    static dependencies = [\"websiteFormOption\"];\n    async load({ editingElement: el }) {\n        return this.dependencies.websiteFormOption.fetchAuthorizedFields(el);\n    }\n    apply({ editingElement: el, value, params, loadResult: authorizedFields }) {\n        // Remove old property fields.\n        for (const [fieldName, field] of Object.entries(authorizedFields)) {\n            if (field._property) {\n                for (const inputEl of el.querySelectorAll(`[name=\"${fieldName}\"]`)) {\n                    inputEl.closest(\".s_website_form_field\").remove();\n                }\n            }\n        }\n        const fieldName = params.fieldName;\n        if (params.isSelect === \"true\") {\n            value = parseInt(value);\n        }\n        this.dependencies.websiteFormOption.addHiddenField(el, value, fieldName);\n    }\n    // TODO clear ? if field is a boolean ?\n    getValue({ editingElement: el, params }) {\n        const value = el.querySelector(\n            `.s_website_form_dnone input[name=\"${params.fieldName}\"]`\n        )?.value;\n        if (params.fieldName === \"email_to\") {\n            // For email_to, we try to find a value in this order:\n            // 1. The current value of the input\n            // 2. The data-for value if it exists\n            // 3. The default value (`defaultEmailToValue`)\n            if (value && value !== DEFAULT_EMAIL_TO_VALUE) {\n                return value;\n            }\n            // Get the email_to value from the data-for attribute if it exists.\n            // We use it if there is no value on the email_to input.\n            const formId = el.id;\n            const dataForValues = getParsedDataFor(formId, el.ownerDocument);\n            return dataForValues?.[\"email_to\"] || DEFAULT_EMAIL_TO_VALUE;\n        }\n        if (value) {\n            return value;\n        } else {\n            return params.isSelect ? \"0\" : \"\";\n        }\n    }\n    isApplied({ editingElement, params, value }) {\n        const currentValue = this.getValue({\n            editingElement,\n            params,\n        });\n        return currentValue === value;\n    }\n}\nexport class PromptSaveRedirectAction extends BuilderAction {\n    static id = \"promptSaveRedirect\";\n    static dependencies = [\"savePlugin\"];\n    apply({ params: { mainParam } }) {\n        const redirectToAction = (action) => {\n            redirect(`/odoo/action-${encodeURIComponent(action)}`);\n        };\n        new Promise((resolve) => {\n            const message = _t(\"You are about to be redirected. Your changes will be saved.\");\n            this.services.dialog.add(ConfirmationDialog, {\n                body: message,\n                confirmLabel: _t(\"Save and Redirect\"),\n                confirm: async () => {\n                    await this.dependencies.savePlugin.save();\n                    await this.config.closeEditor();\n                    redirectToAction(mainParam);\n                    resolve();\n                },\n                cancel: () => resolve(),\n            });\n        });\n    }\n}\nexport class UpdateLabelsMarkAction extends BuilderAction {\n    static id = \"updateLabelsMark\";\n    static dependencies = [\"websiteFormOption\"];\n    apply({ editingElement: el }) {\n        this.dependencies.websiteFormOption.setLabelsMark(el);\n    }\n    isApplied() {\n        return true;\n    }\n}\n\nexport class SetMarkAction extends BuilderAction {\n    static id = \"setMark\";\n    static dependencies = [\"websiteFormOption\"];\n    apply({ editingElement: el, value }) {\n        el.dataset.mark = value.trim();\n        this.dependencies.websiteFormOption.setLabelsMark(el);\n    }\n    getValue({ editingElement: el }) {\n        const mark = getMark(el);\n        return mark;\n    }\n}\n\nexport class OnSuccessAction extends BuilderAction {\n    static id = \"onSuccess\";\n    apply({ editingElement: el, value }) {\n        el.dataset.successMode = value;\n        let messageEl = el.parentElement.querySelector(\".s_website_form_end_message\");\n        if (value === \"message\") {\n            if (!messageEl) {\n                messageEl = renderToElement(\"website.s_website_form_end_message\");\n                el.insertAdjacentElement(\"afterend\", messageEl);\n            }\n        } else {\n            messageEl?.remove();\n            messageEl?.classList.remove(\"o_show_form_success_message\");\n            el.classList.remove(\"o_show_form_success_message\");\n        }\n    }\n    isApplied({ editingElement: el, value }) {\n        const currentValue = el.dataset.successMode;\n        return currentValue === value;\n    }\n}\nexport class ToggleEndMessageAction extends BuilderAction {\n    static id = \"toggleEndMessage\";\n    static dependencies = [\"builderOptions\"];\n    apply({ editingElement: el }) {\n        const messageEl = el.parentElement.querySelector(\".s_website_form_end_message\");\n        messageEl.classList.add(\"o_show_form_success_message\");\n        el.classList.add(\"o_show_form_success_message\");\n        this.dependencies.builderOptions.setNextTarget(messageEl);\n    }\n    clean({ editingElement: el }) {\n        const messageEl = el.parentElement.querySelector(\".s_website_form_end_message\");\n        messageEl.classList.remove(\"o_show_form_success_message\");\n        el.classList.remove(\"o_show_form_success_message\");\n        this.dependencies.builderOptions.setNextTarget(el);\n    }\n    isApplied({ editingElement: el, value }) {\n        return el.classList.contains(\"o_show_form_success_message\");\n    }\n}\nexport class FormToggleRecaptchaLegalAction extends BuilderAction {\n    static id = \"formToggleRecaptchaLegal\";\n    apply({ editingElement: el }) {\n        const labelWidth = el.querySelector(\".s_website_form_label\").style.width;\n        const legalEl = renderToElement(\"website.s_website_form_recaptcha_legal\", {\n            labelWidth: labelWidth,\n        });\n        legalEl.setAttribute(\"contentEditable\", true);\n        el.querySelector(\".s_website_form_submit\").insertAdjacentElement(\"beforebegin\", legalEl);\n    }\n    clean({ editingElement: el }) {\n        const recaptchaLegalEl = el.querySelector(\".s_website_form_recaptcha\");\n        recaptchaLegalEl.remove();\n    }\n    isApplied({ editingElement: el }) {\n        const recaptchaLegalEl = el.querySelector(\".s_website_form_recaptcha\");\n        return !!recaptchaLegalEl;\n    }\n}\n// Field actions\nexport class CustomFieldAction extends BuilderAction {\n    static id = \"customField\";\n    static dependencies = [\"websiteFormOption\"];\n    load(context) {\n        return this.dependencies.websiteFormOption.prepareFields(context);\n    }\n    apply({ editingElement: fieldEl, value, loadResult: fields }) {\n        this.dependencies.websiteFormOption.clearValidationDataset(fieldEl);\n        delete fieldEl.dataset.requirementComparator;\n        const oldLabelText = fieldEl.querySelector(\".s_website_form_label_content\").textContent;\n        const field = getCustomField(value, oldLabelText);\n        setActiveProperties(fieldEl, field);\n        this.dependencies.websiteFormOption.replaceField(fieldEl, field, fields);\n    }\n    isApplied({ editingElement: fieldEl, value }) {\n        const currentValue = isFieldCustom(fieldEl) ? getFieldType(fieldEl) : \"\";\n        return currentValue === value;\n    }\n}\nexport class ExistingFieldAction extends BuilderAction {\n    static id = \"existingField\";\n    static dependencies = [\"websiteFormOption\"];\n    load(context) {\n        return this.dependencies.websiteFormOption.prepareFields(context);\n    }\n    apply({ editingElement: fieldEl, value, loadResult: fields }) {\n        const field = fields[value];\n        setActiveProperties(fieldEl, field);\n        this.dependencies.websiteFormOption.replaceField(fieldEl, field, fields);\n    }\n    isApplied({ editingElement: fieldEl, value }) {\n        const currentValue = isFieldCustom(fieldEl) ? \"\" : getFieldName(fieldEl);\n        return currentValue === value;\n    }\n}\nexport class SelectTypeAction extends BuilderAction {\n    static id = \"selectType\";\n    static dependencies = [\"websiteFormOption\"];\n    load(context) {\n        return this.dependencies.websiteFormOption.prepareFields(context);\n    }\n    apply({ editingElement: fieldEl, value, loadResult: fields }) {\n        const field = getActiveField(fieldEl, { fields });\n        field.type = value;\n        this.dependencies.websiteFormOption.replaceField(fieldEl, field, fields);\n    }\n    isApplied({ editingElement: fieldEl, value }) {\n        const currentValue = getFieldType(fieldEl);\n        return currentValue === value;\n    }\n}\nexport class ExistingFieldSelectTypeAction extends BuilderAction {\n    static id = \"existingFieldSelectType\";\n    static dependencies = [\"websiteFormOption\"];\n    load(context) {\n        return this.dependencies.websiteFormOption.prepareFields(context);\n    }\n    apply({ editingElement: fieldEl, value, loadResult: fields }) {\n        const field = getActiveField(fieldEl, { fields });\n        field.type = value;\n        this.dependencies.websiteFormOption.replaceField(fieldEl, field, fields);\n    }\n    isApplied({ editingElement: fieldEl, value }) {\n        const currentValue = getFieldType(fieldEl);\n        return currentValue === value;\n    }\n}\nexport class MultiCheckboxDisplayAction extends BuilderAction {\n    static id = \"multiCheckboxDisplay\";\n    apply({ editingElement: fieldEl, value }) {\n        const targetEl = getMultipleInputs(fieldEl);\n        const isHorizontal = value === \"horizontal\";\n        for (const el of targetEl.querySelectorAll(\".checkbox, .radio\")) {\n            el.classList.toggle(\"col-lg-4\", isHorizontal);\n            el.classList.toggle(\"col-md-6\", isHorizontal);\n        }\n        targetEl.dataset.display = value;\n    }\n    isApplied({ editingElement: fieldEl, value }) {\n        const targetEl = getMultipleInputs(fieldEl);\n        const currentValue = targetEl ? targetEl.dataset.display : \"\";\n        return currentValue === value;\n    }\n}\nexport class SetLabelTextAction extends BuilderAction {\n    static id = \"setLabelText\";\n    static dependencies = [\"websiteFormOption\"];\n    async apply({ editingElement: fieldEl, value }) {\n        const labelEl = fieldEl.querySelector(\".s_website_form_label_content\");\n        labelEl.textContent = value;\n        if (isFieldCustom(fieldEl)) {\n            value = getQuotesEncodedName(value);\n            const multiple = fieldEl.querySelector(\".s_website_form_multiple\");\n            if (multiple) {\n                multiple.dataset.name = value;\n            }\n            const inputEls = fieldEl.querySelectorAll(\".s_website_form_input\");\n            const previousInputName = inputEls[0].name;\n            inputEls.forEach((el) => (el.name = value));\n\n            // Synchronize the fields whose visibility depends on this field\n            const dependentEls = fieldEl.closest(\"form\").querySelectorAll(\n                `.s_website_form_field[data-visibility-dependency=\"${CSS.escape(\n                    previousInputName\n                )}\"],\n                    .s_website_form_field[data-visibility-dependency=\"${CSS.escape(value)}\"]`\n            );\n            for (const dependentEl of dependentEls) {\n                if (findCircular(fieldEl, dependentEl)) {\n                    // For all the fields whose visibility depends on this\n                    // field, check if the new name creates a circular\n                    // dependency and remove the problematic conditional\n                    // visibility if it is the case. E.g. a field (A) depends on\n                    // another (B) and the user renames \"B\" by \"A\".\n                    deleteConditionalVisibility(dependentEl);\n                } else {\n                    dependentEl.dataset.visibilityDependency = value;\n                }\n            }\n            const fieldWithVisibilityDependencyEls = [\n                ...fieldEl.closest(\"form\").querySelectorAll(\"[data-visibility-dependency]\"),\n            ];\n            await Promise.all(\n                fieldWithVisibilityDependencyEls.map(async (fieldWithConditionEl) => {\n                    const conditionFieldName = fieldWithConditionEl.dataset.visibilityDependency;\n                    const fieldData = await this.dependencies.websiteFormOption.loadFieldOptionData(\n                        fieldWithConditionEl\n                    );\n                    const names = fieldData.conditionInputs.map((entry) => entry.name);\n                    if (!names.includes(conditionFieldName)) {\n                        deleteConditionalVisibility(fieldWithConditionEl);\n                    }\n                })\n            );\n        }\n    }\n    getValue({ editingElement: fieldEl }) {\n        const labelEl = fieldEl.querySelector(\".s_website_form_label_content\");\n        return labelEl.textContent;\n    }\n}\nexport class SelectLabelPositionAction extends BuilderAction {\n    static id = \"selectLabelPosition\";\n    static dependencies = [\"websiteFormOption\"];\n    load(context) {\n        return this.dependencies.websiteFormOption.prepareFields(context);\n    }\n    apply({ editingElement: fieldEl, value, loadResult: fields }) {\n        const field = getActiveField(fieldEl, { fields });\n        field.formatInfo.labelPosition = value;\n        this.dependencies.websiteFormOption.replaceField(fieldEl, field, fields);\n    }\n    isApplied({ editingElement: fieldEl, value }) {\n        const currentValue = getLabelPosition(fieldEl);\n        return currentValue === value;\n    }\n}\nexport class ToggleDescriptionAction extends BuilderAction {\n    static id = \"toggleDescription\";\n    static dependencies = [\"websiteFormOption\"];\n    load(context) {\n        return this.dependencies.websiteFormOption.prepareFields(context);\n    }\n    apply({ editingElement: fieldEl, loadResult: fields, value }) {\n        const description = fieldEl.querySelector(\".s_website_form_field_description\");\n        const hasDescription = !!description;\n        const field = getActiveField(fieldEl, { fields });\n        field.description = !hasDescription; // Will be changed to default description in qweb\n        this.dependencies.websiteFormOption.replaceField(fieldEl, field, fields);\n    }\n    isApplied({ editingElement: fieldEl }) {\n        const description = fieldEl.querySelector(\".s_website_form_field_description\");\n        return !!description;\n    }\n}\nexport class SelectTextareaValueAction extends BuilderAction {\n    static id = \"selectTextareaValue\";\n    apply({ editingElement: fieldEl, value }) {\n        fieldEl.textContent = value;\n        fieldEl.value = value;\n    }\n    getValue({ editingElement: fieldEl }) {\n        return fieldEl.textContent;\n    }\n}\nexport class ToggleRequiredAction extends BuilderAction {\n    static id = \"toggleRequired\";\n    static dependencies = [\"websiteFormOption\"];\n    apply({ editingElement: fieldEl, params: { mainParam: activeValue } }) {\n        fieldEl.classList.add(activeValue);\n        fieldEl\n            .querySelectorAll(\"input, select, textarea\")\n            .forEach((el) => el.toggleAttribute(\"required\", true));\n        this.dependencies.websiteFormOption.setLabelsMark(fieldEl.closest(\"form\"));\n    }\n    clean({ editingElement: fieldEl, params: { mainParam: activeValue } }) {\n        fieldEl.classList.remove(activeValue);\n        fieldEl\n            .querySelectorAll(\"input, select, textarea\")\n            .forEach((el) => el.removeAttribute(\"required\"));\n        this.dependencies.websiteFormOption.setLabelsMark(fieldEl.closest(\"form\"));\n    }\n    isApplied({ editingElement: fieldEl, params: { mainParam: activeValue } }) {\n        return fieldEl.classList.contains(activeValue);\n    }\n}\n\n/**\n * Custom error message should be visible or not.\n */\nexport class SetRequirementComparatorAction extends BuilderAction {\n    static id = \"setRequirementComparator\";\n    static dependencies = [\"websiteFormOption\"];\n    apply({ editingElement: fieldEl }) {\n        this.dependencies.websiteFormOption.clearValidationDataset(fieldEl);\n    }\n}\n/**\n * Sets the dataset value of custom-error attribute which is further used to\n * determine if the input for custom error message should be visible or not.\n *\n * TODO this is basically a toggle whose only purpose is to show more options\n * in the sidebar... its status should not be saved in the website DOM...\n */\nexport class SetCustomErrorMessageAction extends BuilderAction {\n    static id = \"setCustomErrorMessage\";\n    apply({ editingElement: fieldEl }) {\n        if (!fieldEl.dataset.customError) {\n            fieldEl.dataset.customError = true;\n        } else {\n            delete fieldEl.dataset.customError;\n        }\n    }\n    isApplied({ editingElement: fieldEl }) {\n        return fieldEl.dataset.customError;\n    }\n}\n/**\n * Sets the default error message based on the requirement comparator,\n * condition and type of form fields.\n */\nexport class SetDefaultErrorMessageAction extends BuilderAction {\n    static id = \"setDefaultErrorMessage\";\n    static dependencies = [\"websiteFormOption\"];\n    apply({ editingElement: fieldEl }) {\n        const {\n            requirementComparator: comparator,\n            requirementCondition: condition,\n            requirementBetween: between,\n            type,\n        } = fieldEl.dataset;\n        fieldEl.dataset.errorMessage = this.dependencies.websiteFormOption.defaultMessage(\n            comparator,\n            condition,\n            between,\n            type\n        );\n    }\n}\n\nexport class SetVisibilityAction extends BuilderAction {\n    static id = \"setVisibility\";\n    static dependencies = [\"websiteFormOption\"];\n    load(context) {\n        return this.dependencies.websiteFormOption.prepareConditionInputs(context);\n    }\n    apply({ editingElement: fieldEl, value, loadResult: conditionInputs }) {\n        if (value === \"conditional\") {\n            for (const conditionInput of conditionInputs) {\n                if (conditionInput.name) {\n                    // Set a default visibility dependency\n                    setVisibilityDependency(fieldEl, conditionInput.name);\n                    return;\n                }\n            }\n            this.services.dialog.add(ConfirmationDialog, {\n                body: _t(\"There is no field available for this option.\"),\n            });\n        }\n        deleteConditionalVisibility(fieldEl);\n    }\n    isApplied() {\n        return true;\n    }\n}\nexport class SetVisibilityDependencyAction extends BuilderAction {\n    static id = \"setVisibilityDependency\";\n    apply({ editingElement: fieldEl, value }) {\n        return setVisibilityDependency(fieldEl, value);\n    }\n    isApplied({ editingElement: fieldEl, value }) {\n        const currentValue = fieldEl.dataset.visibilityDependency || \"\";\n        return currentValue === value;\n    }\n}\nexport class SetFormCustomFieldValueListAction extends BuilderAction {\n    static id = \"setFormCustomFieldValueList\";\n    static dependencies = [\"websiteFormOption\"];\n    load(context) {\n        return this.dependencies.websiteFormOption.prepareFields(context);\n    }\n    apply({ editingElement: fieldEl, value, loadResult: fields }) {\n        let valueList = JSON.parse(value);\n        if (getSelect(fieldEl)) {\n            valueList = valueList.filter((value) => value.id !== \"\" || value.display_name !== \"\");\n            const hasDefault = valueList.some((value) => value.selected);\n            if (valueList.length && !hasDefault) {\n                valueList.unshift({\n                    id: \"\",\n                    display_name: \"\",\n                    selected: true,\n                });\n            }\n        }\n        const field = getActiveField(fieldEl, { fields });\n        field.records = valueList;\n        this.dependencies.websiteFormOption.replaceField(fieldEl, field, fields);\n    }\n    getValue({ editingElement: fieldEl }) {\n        const fields = [];\n        const field = getActiveField(fieldEl, { fields });\n        if (\n            field.records.length &&\n            field.records[0].display_name === \"\" &&\n            field.records[0].selected === true\n        ) {\n            field.records.shift();\n        }\n        return JSON.stringify(field.records);\n    }\n}\nclass PropertyAction extends BuilderAction {\n    static id = \"property\";\n\n    apply({ editingElement, params: { property, format } = {}, value }) {\n        editingElement[property] = format ? format(value) : value;\n    }\n}\nclass SetMultipleFilesAction extends BuilderAction {\n    static id = \"setMultipleFiles\";\n    apply({ editingElement }) {\n        editingElement.multiple = editingElement.dataset.maxFilesNumber > 1;\n    }\n}\n\nregistry.category(\"website-plugins\").add(FormOptionPlugin.id, FormOptionPlugin);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { escape } from \"@web/core/utils/strings\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { generateHTMLId } from \"@html_builder/utils/utils_css\";\nimport { isSmallInteger } from \"@html_builder/utils/utils\";\n\nexport const VISIBILITY_DATASET = [\n    \"visibilityDependency\",\n    \"visibilityCondition\",\n    \"visibilityComparator\",\n    \"visibilityBetween\",\n];\n\n/**\n * Returns the parsed data coming from the data-for element for the given form.\n * TODO Note that we should rely on the same util as the website form interaction.\n * Maybe this will need to be deleted.\n *\n * @param {string} formId\n * @param {HTMLElement} parentEl\n * @returns {Object|undefined} the parsed data\n */\nexport function getParsedDataFor(formId, parentEl) {\n    const dataForEl = parentEl.querySelector(`[data-for='${formId}']`);\n    if (!dataForEl) {\n        return;\n    }\n    return JSON.parse(\n        dataForEl.dataset.values\n            // replaces `True` by `true` if they are after `,` or `:` or `[`\n            .replace(/([,:[]\\s*)True/g, \"$1true\")\n            // replaces `False` and `None` by `\"\"` if they are after `,` or `:` or `[`\n            .replace(/([,:[]\\s*)(False|None)/g, '$1\"\"')\n            // replaces the `'` by `\"` if they are before `,` or `:` or `]` or `}`\n            .replace(/'(\\s*[,:\\]}])/g, '\"$1')\n            // replaces the `'` by `\"` if they are after `{` or `[` or `,` or `:`\n            .replace(/([{[:,]\\s*)'/g, '$1\"')\n    );\n}\n\n/**\n * Returns a field object\n *\n * @param {string} type the type of the field\n * @param {string} label The label of the field. Also used as the field's\n *                       name if no `name` is provided.\n * @param {string} [name] The name of the field. Falls back to `label` if\n *                        not specified\n * @returns {Object}\n */\nexport function getCustomField(type, label, name = \"\") {\n    return {\n        name: name || label,\n        string: label,\n        custom: true,\n        type: type,\n        // Default values for x2many fields and selection\n        records: [\n            {\n                id: _t(\"Option 1\"),\n                display_name: _t(\"Option 1\"),\n            },\n            {\n                id: _t(\"Option 2\"),\n                display_name: _t(\"Option 2\"),\n            },\n            {\n                id: _t(\"Option 3\"),\n                display_name: _t(\"Option 3\"),\n            },\n        ],\n    };\n}\n\nexport const getMark = (el) => el.dataset.mark;\nexport const isOptionalMark = (el) => el.classList.contains(\"o_mark_optional\");\nexport const isRequiredMark = (el) => el.classList.contains(\"o_mark_required\");\n/**\n * Returns the default formatInfos of a field.\n *\n * @param {HTMLElement} el\n * @returns {Object}\n */\nexport function getDefaultFormat(el) {\n    return {\n        labelWidth: el.querySelector(\".s_website_form_label\").style.width,\n        labelPosition: \"left\",\n        multiPosition: \"horizontal\",\n        requiredMark: isRequiredMark(el),\n        optionalMark: isOptionalMark(el),\n        mark: getMark(el),\n    };\n}\n\n/**\n * Replace all `\"` character by `&quot;`.\n *\n * @param {string} name\n * @returns {string}\n */\nexport function getQuotesEncodedName(name) {\n    // Browsers seem to be encoding the double quotation mark character as\n    // `%22` (URI encoded version) when used inside an input's name. It is\n    // actually quite weird as a sent `<input name='Hello \"world\" %22'/>`\n    // will actually be received as `Hello %22world%22 %22` on the server,\n    // making it impossible to know which is actually a real double\n    // quotation mark and not the \"%22\" string. Values do not have this\n    // problem: `Hello \"world\" %22` would be received as-is on the server.\n    // In the future, we should consider not using label values as input\n    // names anyway; the idea was bad in the first place. We should probably\n    // assign random field names (as we do for IDs) and send a mapping\n    // with the labels, as values (TODO ?).\n    return name.replaceAll(/\"/g, (character) => `&quot;`);\n}\n\n/**\n * Renders a field of the form based on its description\n *\n * @param {Object} field\n * @returns {HTMLElement}\n */\nexport function renderField(field, resetId = false) {\n    if (!field.id) {\n        field.id = generateHTMLId();\n    }\n    const params = { field: { ...field }, defaultName: escape(field.string || _t(\"Field\")) };\n    if ([\"url\", \"email\", \"tel\"].includes(field.type)) {\n        params.field.inputType = field.type;\n    }\n    if ([\"boolean\", \"selection\", \"binary\"].includes(field.type)) {\n        params.field.isCheck = true;\n    }\n    if (field.type === \"one2many\" && field.relation !== \"ir.attachment\") {\n        params.field.isCheck = true;\n    }\n    if (field.custom && !field.string) {\n        params.field.string = field.name;\n    }\n    if (field.description) {\n        params.default_description = _t(\"Describe your field here.\");\n    } else if ([\"email_cc\", \"email_to\"].includes(field.name)) {\n        params.default_description = _t(\"Separate email addresses with a comma.\");\n    }\n    const template = document.createElement(\"template\");\n    const renderType = field.type === \"tags\" ? \"many2many\" : field.type;\n    template.content.append(renderToElement(\"website.form_field_\" + renderType, params));\n    if (field.description && field.description !== true) {\n        const descriptionEl = template.content.querySelector(\".s_website_form_field_description\");\n        descriptionEl.replaceWith(field.description);\n    }\n    template.content\n        .querySelectorAll(\"input.datetimepicker-input\")\n        .forEach((el) => (el.value = field.propertyValue));\n    template.content.querySelectorAll(\"[name]\").forEach((el) => {\n        el.name = getQuotesEncodedName(el.name);\n    });\n    template.content.querySelectorAll(\"[data-name]\").forEach((el) => {\n        el.dataset.name = getQuotesEncodedName(el.dataset.name);\n    });\n    // TODO remove this part in master and add offset classes in xml\n    template.content.querySelectorAll(\".s_website_form_field\").forEach((el) => {\n        if (field.formatInfo.offset) {\n            el.classList.add(field.formatInfo.offset);\n        }\n    });\n    return template.content.firstElementChild;\n}\n\n/**\n * Returns true if the field is required by the model or by the user.\n *\n * @param {HTMLElement} fieldEl\n * @returns {boolean}\n */\nexport function isFieldRequired(fieldEl) {\n    const classList = fieldEl.classList;\n    return (\n        classList.contains(\"s_website_form_required\") ||\n        classList.contains(\"s_website_form_model_required\")\n    );\n}\n\n/**\n * Returns the multiple checkbox/radio element if it exist else null\n *\n * @param {HTMLElement} fieldEl\n * @returns {HTMLElement}\n */\nexport function getMultipleInputs(fieldEl) {\n    return fieldEl.querySelector(\".s_website_form_multiple\");\n}\n\nexport function getLabelPosition(fieldEl) {\n    const label = fieldEl.querySelector(\".s_website_form_label\");\n    if (fieldEl.querySelector(\".row:not(.s_website_form_multiple)\")) {\n        return label.classList.contains(\"text-end\") ? \"right\" : \"left\";\n    } else {\n        return label.classList.contains(\"d-none\") ? \"none\" : \"top\";\n    }\n}\n\n/**\n * Returns the format object of a field containing\n * the position, labelWidth and bootstrap col class\n *\n * @param {HTMLElement} fieldEl\n * @returns {Object}\n */\nexport function getFieldFormat(fieldEl) {\n    let requiredMark, optionalMark;\n    const mark = fieldEl.querySelector(\".s_website_form_mark\");\n    if (mark) {\n        requiredMark = isFieldRequired(fieldEl);\n        optionalMark = !requiredMark;\n    }\n    const multipleInputEl = getMultipleInputs(fieldEl);\n    const format = {\n        labelPosition: getLabelPosition(fieldEl),\n        labelWidth: fieldEl.querySelector(\".s_website_form_label\").style.width,\n        multiPosition: (multipleInputEl && multipleInputEl.dataset.display) || \"horizontal\",\n        col: [...fieldEl.classList].filter((el) => el.match(/^col-/g)).join(\" \"),\n        offset: [...fieldEl.classList].filter((el) => el.match(/^offset-/g)).join(\" \"),\n        requiredMark: requiredMark,\n        optionalMark: optionalMark,\n        mark: mark && mark.textContent,\n    };\n    return format;\n}\n\n/**\n * Returns true if the field is a custom field, false if it is an existing field\n *\n * @param {HTMLElement} fieldEl\n * @returns {boolean}\n */\nexport function isFieldCustom(fieldEl) {\n    return !!fieldEl.classList.contains(\"s_website_form_custom\");\n}\n\n/**\n * Returns the name of the field\n *\n * @param {HTMLElement} fieldEl\n * @returns {string}\n */\nexport function getFieldName(fieldEl = this.$target[0]) {\n    const multipleName = fieldEl.querySelector(\".s_website_form_multiple\");\n    return multipleName\n        ? multipleName.dataset.name\n        : fieldEl.querySelector(\".s_website_form_input\").name;\n}\n/**\n * Returns the type of the  field, can be used for both custom and existing fields\n *\n * @param {HTMLElement} fieldEl\n * @returns {string}\n */\nexport function getFieldType(fieldEl) {\n    return fieldEl.dataset.type;\n}\n\n/**\n * Set the active field properties on the field Object\n *\n * @param {HTMLElement} fieldEl\n * @param {Object} field Field to complete with the active field info\n */\nexport function setActiveProperties(fieldEl, field) {\n    const classList = fieldEl.classList;\n    const textarea = fieldEl.querySelector(\"textarea\");\n    const input = fieldEl.querySelector(\n        'input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"tel\"], input[type=\"url\"], textarea'\n    );\n    const fileInputEl = fieldEl.querySelector(\"input[type=file]\");\n    const description = fieldEl.querySelector(\".s_website_form_field_description\");\n    field.placeholder = input?.placeholder || \"\";\n    if (input) {\n        // textarea value has no attribute,  date/datetime timestamp property is formated\n        field.value = input.getAttribute(\"value\") || input.value;\n    } else if (field.type === \"boolean\") {\n        field.value = !!fieldEl.querySelector('input[type=\"checkbox\"][checked]');\n    } else if (fileInputEl) {\n        field.maxFilesNumber = fileInputEl.dataset.maxFilesNumber;\n        field.maxFileSize = fileInputEl.dataset.maxFileSize;\n    }\n    // property value is needed for date/datetime (formated date).\n    field.propertyValue = input && input.value;\n    field.description = description;\n    field.rows = textarea && textarea.rows;\n    field.required = classList.contains(\"s_website_form_required\");\n    field.modelRequired = classList.contains(\"s_website_form_model_required\");\n    field.hidden = classList.contains(\"s_website_form_field_hidden\");\n    field.formatInfo = getFieldFormat(fieldEl);\n}\n\n/**\n * Replaces the target with provided field.\n *\n * @param {HTMLElement} oldFieldEl\n * @param {HTMLElement} fieldEl\n */\nexport function replaceFieldElement(oldFieldEl, fieldEl) {\n    const inputEl = oldFieldEl.querySelector(\"input\");\n    const dataFillWith = inputEl ? inputEl.dataset.fillWith : undefined;\n    const hasConditionalVisibility = oldFieldEl.classList.contains(\n        \"s_website_form_field_hidden_if\"\n    );\n    const previousInputEl = oldFieldEl.querySelector(\".s_website_form_input\");\n    const previousName = previousInputEl.name;\n    const previousType = previousInputEl.type;\n    [...oldFieldEl.childNodes].forEach((node) => node.remove());\n    [...fieldEl.childNodes].forEach((node) => oldFieldEl.appendChild(node));\n    [...fieldEl.attributes].forEach((el) => oldFieldEl.removeAttribute(el.nodeName));\n    [...fieldEl.attributes].forEach((el) => oldFieldEl.setAttribute(el.nodeName, el.nodeValue));\n    if (hasConditionalVisibility) {\n        oldFieldEl.classList.add(\"s_website_form_field_hidden_if\", \"d-none\");\n    }\n    const dependentFieldEls = oldFieldEl\n        .closest(\"form\")\n        .querySelectorAll(\n            `.s_website_form_field[data-visibility-dependency=\"${CSS.escape(previousName)}\"]`\n        );\n    const newFormInputEl = oldFieldEl.querySelector(\".s_website_form_input\");\n    const newName = newFormInputEl.name;\n    const newType = newFormInputEl.type;\n    if ((previousName !== newName || previousType !== newType) && dependentFieldEls) {\n        // In order to keep the visibility conditions consistent,\n        // when the name has changed, it means that the type has changed so\n        // all fields whose visibility depends on this field must be updated so that\n        // they no longer have conditional visibility\n        for (const fieldEl of dependentFieldEls) {\n            deleteConditionalVisibility(fieldEl);\n        }\n    }\n    const newInputEl = oldFieldEl.querySelector(\"input\");\n    if (newInputEl) {\n        newInputEl.dataset.fillWith = dataFillWith;\n    }\n}\n\n/**\n * Returns the target as a field Object\n *\n * @param {HTMLElement} fieldEl\n * @param {boolean} noRecords\n * @returns {Object}\n */\nexport function getActiveField(fieldEl, { noRecords, fields } = {}) {\n    let field;\n    const labelText = fieldEl.querySelector(\".s_website_form_label_content\")?.innerText || \"\";\n    if (isFieldCustom(fieldEl)) {\n        const inputName = fieldEl.querySelector(\".s_website_form_input\").getAttribute(\"name\");\n        field = getCustomField(fieldEl.dataset.type, labelText, inputName);\n    } else {\n        field = Object.assign({}, fields[getFieldName(fieldEl)]);\n        field.string = labelText;\n        field.type = getFieldType(fieldEl);\n    }\n    if (!noRecords) {\n        field.records = getListItems(fieldEl);\n    }\n    setActiveProperties(fieldEl, field);\n    return field;\n}\n\n/**\n * Deletes all attributes related to conditional visibility.\n *\n * @param {HTMLElement} fieldEl\n */\nexport function deleteConditionalVisibility(fieldEl) {\n    for (const name of VISIBILITY_DATASET) {\n        delete fieldEl.dataset[name];\n    }\n    fieldEl.classList.remove(\"s_website_form_field_hidden_if\", \"d-none\");\n}\n\n/**\n * Returns the select element if it exist else null\n *\n * @param {HTMLElement} fieldEl\n * @returns {HTMLElement}\n */\nexport function getSelect(fieldEl) {\n    return fieldEl.querySelector(\"select\");\n}\n\n/**\n * Returns the next new record id.\n *\n * @param {HTMLElement} fieldEl\n */\nexport function getNewRecordId(fieldEl) {\n    const selectEl = getSelect(fieldEl);\n    const multipleInputsEl = getMultipleInputs(fieldEl);\n    let options = [];\n    if (selectEl) {\n        options = [...selectEl.querySelectorAll(\"option\")];\n    } else if (multipleInputsEl) {\n        options = [...multipleInputsEl.querySelectorAll(\".checkbox input, .radio input\")];\n    }\n    // TODO: @owl-option factorize code above\n    const targetEl = fieldEl.querySelector(\".s_website_form_input\");\n    let id;\n    if ([\"checkbox\", \"radio\"].includes(targetEl.getAttribute(\"type\"))) {\n        // Remove first checkbox/radio's id's final '0'.\n        id = targetEl.id.slice(0, -1);\n    } else {\n        id = targetEl.id;\n    }\n    return id + options.length;\n}\n\n/**\n * @param {HTMLElement} fieldEl\n * @returns {HTMLElement} The visibility dependency of the field\n */\nexport function getDependencyEl(fieldEl) {\n    const dependencyName = fieldEl.dataset.visibilityDependency;\n    return fieldEl\n        .closest(\"form\")\n        ?.querySelector(`.s_website_form_input[name=\"${CSS.escape(dependencyName)}\"]`);\n}\n\n/**\n * @param {HTMLElement} fieldEl\n * @returns {HTMLElement} The current field input\n */\nexport function getCurrentFieldInputEl(fieldEl) {\n    return fieldEl.querySelector(\".s_website_form_input\");\n}\n\n/**\n * @param {HTMLElement} dependentFieldEl\n * @param {HTMLElement} targetFieldEl\n * @returns {boolean} \"true\" if adding \"dependentFieldEl\" or any other field\n * with the same label in the conditional visibility of \"targetFieldEl\"\n * would create a circular dependency involving \"targetFieldEl\".\n */\nexport function findCircular(dependentFieldEl, targetFieldEl) {\n    const formEl = targetFieldEl.closest(\"form\");\n    // Keep a register of the already visited fields to not enter an\n    // infinite check loop.\n    const visitedFields = new Set();\n    const recursiveFindCircular = (dependentFieldEl, targetFieldEl) => {\n        const dependentFieldName = getFieldName(dependentFieldEl);\n        // Get all the fields that have the same label as the dependent\n        // field.\n        let dependentFieldEls = Array.from(\n            formEl.querySelectorAll(\n                `.s_website_form_input[name=\"${CSS.escape(dependentFieldName)}\"]`\n            )\n        ).map((el) => el.closest(\".s_website_form_field\"));\n        // Remove the duplicated fields. This could happen if the field has\n        // multiple inputs (\"Multiple Checkboxes\" for example.)\n        dependentFieldEls = new Set(dependentFieldEls);\n        const fieldName = getFieldName(targetFieldEl);\n        for (const dependentFieldEl of dependentFieldEls) {\n            // Only check for circular dependencies on fields that do not\n            // already have been checked.\n            if (!visitedFields.has(dependentFieldEl)) {\n                // Add the dependentFieldEl in the set of checked field.\n                visitedFields.add(dependentFieldEl);\n                if (dependentFieldEl.dataset.visibilityDependency === fieldName) {\n                    return true;\n                }\n                const dependencyInputEl = getDependencyEl(dependentFieldEl);\n                if (\n                    dependencyInputEl &&\n                    recursiveFindCircular(\n                        dependencyInputEl.closest(\".s_website_form_field\"),\n                        targetFieldEl\n                    )\n                ) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    };\n    return recursiveFindCircular(dependentFieldEl, targetFieldEl);\n}\n\n/**\n * Returns the domain of a field.\n *\n * @param {HTMLElement} formEl\n * @param {String} name\n * @param {String} type\n * @param {String} relation\n * @returns {Object|false}\n */\n// TODO Solve this variable differently\nconst allFormsInfo = new Map();\nexport function getDomain(formEl, name, type, relation) {\n    // We need this because the field domain is in formInfo in the\n    // WebsiteFormEditor but we need it in the WebsiteFieldEditor.\n    if (!allFormsInfo.get(formEl) || !name || !type || !relation) {\n        return false;\n    }\n    const field = allFormsInfo\n        .get(formEl)\n        .fields.find((el) => el.name === name && el.type === type && el.relation === relation);\n    return field && field.domain;\n}\n\nexport function getModelName(formEl) {\n    return formEl.dataset.model_name || \"mail.mail\";\n}\n\nexport function getListItems(fieldEl) {\n    const selectEl = getSelect(fieldEl);\n    const multipleInputsEl = getMultipleInputs(fieldEl);\n    let options = [];\n    if (selectEl) {\n        options = [...selectEl.querySelectorAll(\"option\")];\n    } else if (multipleInputsEl) {\n        options = [...multipleInputsEl.querySelectorAll(\".checkbox input, .radio input\")];\n    }\n    return options.map((opt) => {\n        const name = selectEl ? opt : opt.nextElementSibling;\n        return {\n            id: isSmallInteger(opt.value) ? parseInt(opt.value) : opt.value,\n            display_name: name.textContent.trim(),\n            selected: selectEl ? opt.selected : opt.checked,\n        };\n    });\n}\n\n/**\n * Sets the visibility dependency of the field.\n *\n * @param {HTMLElement} fieldEl\n * @param {string} value name of the dependency input\n */\nexport function setVisibilityDependency(fieldEl, value) {\n    delete fieldEl.dataset.visibilityCondition;\n    delete fieldEl.dataset.visibilityComparator;\n    fieldEl.dataset.visibilityDependency = value;\n}\n\n/**\n * Re-renders a form field in the DOM.\n *\n * @param {HTMLElement} fieldEl - The original field element to be re-rendered.\n * @param {Object<string, Object>} fields - A map of all fields in the form.\n */\nexport function rerenderField(fieldEl, fields) {\n    const field = getActiveField(fieldEl, { fields });\n    delete field.id;\n    const newFieldEl = renderField(field);\n    replaceFieldElement(fieldEl, newFieldEl);\n}\n", "import { Component, onWillStart, useState } from \"@odoo/owl\";\nimport { ColorPicker } from \"@web/core/color_picker/color_picker\";\nimport { HighlightPicker } from \"./highlight_picker\";\nimport { normalizeColor } from \"@html_builder/utils/utils_css\";\nimport { getHtmlStyle } from \"@html_editor/utils/formatting\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport const highlightIdToName = {\n    underline: \"Underline\",\n    freehand_1: \"Freehand 1\",\n    freehand_2: \"Freehand 2\",\n    freehand_3: \"Freehand 3\",\n    double: \"Double\",\n    wavy: \"Wavy\",\n    circle_1: \"Circle 1\",\n    circle_2: \"Circle 2\",\n    circle_3: \"Circle 3\",\n    over_underline: \"Over and underline\",\n    scribble_1: \"Scribble 1\",\n    scribble_2: \"Scribble 2\",\n    scribble_3: \"Scribble 3\",\n    scribble_4: \"Scribble 4\",\n    jagged: \"Jagged\",\n    cross: \"Cross\",\n    diagonal: \"Diagonal\",\n    strikethrough: \"Strikethrough\",\n    bold: \"Bold\",\n    bold_1: \"Bold 1\",\n    bold_2: \"Bold 2\",\n};\n\nexport class HighlightConfigurator extends Component {\n    static template = \"website.highlightConfigurator\";\n    static components = { ColorPicker };\n    static props = {\n        applyHighlight: Function,\n        applyHighlightStyle: Function,\n        deleteHighlight: Function,\n        getHighlightState: Function,\n        previewHighlight: Function,\n        previewHighlightStyle: Function,\n        revertHighlight: Function,\n        revertHighlightStyle: Function,\n        componentStack: Object,\n        getUsedCustomColors: Function,\n        getMaxFontSize: Function,\n    };\n\n    setup() {\n        this.state = useState(this.props.getHighlightState());\n        this.highlightIdToName = highlightIdToName;\n        onWillStart(() => {\n            if (!this.state.highlightId) {\n                this.openHighlightPicker(false);\n            }\n        });\n    }\n\n    openHighlightPicker(withPrevious = true) {\n        // Picker's samples use the fs-3 class\n        const fs3 = document.createElement(\"div\");\n        fs3.classList.add(\"fs-3\");\n        document.body.append(fs3);\n        const fs3Size = parseFloat(getComputedStyle(fs3).fontSize);\n        fs3.remove();\n        const fontRatio = this.props.getMaxFontSize() / fs3Size;\n        this.props.componentStack.push(\n            HighlightPicker,\n            {\n                selectHighlight: this.selectHighlight.bind(this),\n                previewHighlight: this.props.previewHighlight,\n                revertHighlight: this.props.revertHighlight,\n                style: `\n                    --text-highlight-width: ${(this.state.thickness || 2) / fontRatio}px;\n                    --text-highlight-color: ${this.state.color || \"var(--o-color-1)\"};\n                `,\n            },\n            _t(\"Select a highlight\"),\n            withPrevious\n        );\n    }\n\n    openColorPicker() {\n        this.props.componentStack.push(\n            ColorPicker,\n            {\n                state: { selectedColor: this.state.color, defaultTab: \"solid\" },\n                colorPrefix: \"hb-cp-\",\n                getUsedCustomColors: this.props.getUsedCustomColors,\n                enabledTabs: [\"solid\", \"custom\"],\n                applyColor: this.selectHighlightColor.bind(this),\n                applyColorPreview: (color) =>\n                    this.props.previewHighlightStyle(\n                        \"--text-highlight-color\",\n                        normalizeColor(color, getHtmlStyle(document))\n                    ),\n                applyColorResetPreview: this.props.revertHighlightStyle,\n                className: \"d-contents\",\n                cssVarColorPrefix: \"hb-cp-\",\n            },\n            \"Select a color\",\n            true\n        );\n    }\n\n    selectHighlight(highlightId) {\n        this.props.componentStack.pop();\n        this.props.applyHighlight(highlightId);\n    }\n\n    selectHighlightColor(color) {\n        this.props.componentStack.pop();\n        const highlightColor = color.startsWith(\"hb-cp-\")\n            ? `var(--${color.replace(\"hb-cp-\", \"\")})`\n            : normalizeColor(color, getHtmlStyle(document));\n        this.props.applyHighlightStyle(\"--text-highlight-color\", highlightColor);\n    }\n\n    deleteHighlight() {\n        this.props.deleteHighlight();\n        this.openHighlightPicker(false);\n    }\n\n    onThicknessChange(ev) {\n        this.props.applyHighlightStyle(\n            \"--text-highlight-width\",\n            ev.target.value ? ev.target.value + \"px\" : \"\"\n        );\n    }\n}\n", "import { onMounted, useRef, Component, onWillDestroy } from \"@odoo/owl\";\nimport {\n    applyTextHighlight,\n    textHighlightFactory,\n    getCurrentTextHighlight,\n} from \"@website/js/highlight_utils\";\n\nexport class HighlightPicker extends Component {\n    static template = \"website.highlightPicker\";\n    static props = {\n        selectHighlight: Function,\n        previewHighlight: Function,\n        revertHighlight: Function,\n        style: { type: String, optional: true },\n    };\n\n    setup() {\n        const root = useRef(\"root\");\n        onMounted(() => {\n            for (const textEl of root.el.querySelectorAll(\".o_text_highlight\")) {\n                const highlightId = getCurrentTextHighlight(textEl);\n                applyTextHighlight(textEl, highlightId);\n            }\n        });\n\n        onWillDestroy(() => {\n            this.props.revertHighlight();\n        });\n    }\n    getHighlightFactory() {\n        return textHighlightFactory;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { Component, xml, useRef, reactive, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { registry } from \"@web/core/registry\";\nimport { HighlightConfigurator } from \"./highlight_configurator\";\nimport { StackingComponent, useStackingComponentState } from \"./stacking_component\";\nimport { formatsSpecs } from \"@html_editor/utils/formatting\";\nimport { closestElement, descendants } from \"@html_editor/utils/dom_traversal\";\nimport { removeClass, removeStyle } from \"@html_editor/utils/dom\";\nimport { isTextNode } from \"@html_editor/utils/dom_info\";\nimport { getCurrentTextHighlight } from \"@website/js/highlight_utils\";\nimport { isCSSColor, rgbaToHex } from \"@web/core/utils/colors\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\nimport { nodeSize } from \"@html_editor/utils/position\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\n\nexport class HighlightPlugin extends Plugin {\n    static id = \"highlight\";\n    static dependencies = [\"history\", \"selection\", \"split\", \"format\", \"edit_interaction\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        toolbar_groups: [withSequence(50, { id: \"websiteDecoration\" })],\n        toolbar_items: [\n            {\n                id: \"highlight\",\n                groupId: \"websiteDecoration\",\n                description: _t(\"Apply highlight\"),\n                Component: HighlightToolbarButton,\n                props: {\n                    highlightConfiguratorProps: {\n                        applyHighlight: this.applyHighlight.bind(this),\n                        previewHighlight: this.previewHighlight.bind(this),\n                        revertHighlight: this.revertHighlight.bind(this),\n                        applyHighlightStyle: this.applyHighlightStyle.bind(this),\n                        previewHighlightStyle: this.previewHighlightStyle.bind(this),\n                        revertHighlightStyle: this.revertHighlightStyle.bind(this),\n                        getHighlightState: () => this.highlightState,\n                        getUsedCustomColors: this.getUsedCustomColors.bind(this),\n                        deleteHighlight: this.deleteSelectedHighlight.bind(this),\n                        getMaxFontSize: this.getMaxFontSize.bind(this),\n                    },\n                    onClick: this.completeHighlightSelection.bind(this),\n                },\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        toolbar_namespace_providers: [\n            withSequence(\n                90,\n                (targetedNodes, editableSelection) =>\n                    closestElement(editableSelection.anchorNode, \".o_text_highlight\") && \"compact\"\n            ),\n        ],\n        normalize_handlers: (root) => {\n            for (const node of root.querySelectorAll(\".o_text_highlight\")) {\n                // Signal to the interaction that there is (maybe) a new element\n                node.dispatchEvent(new Event(\"text_highlight_added\", { bubbles: true }));\n            }\n        },\n        format_class_predicates: (className) => className.startsWith(\"o_text_highlight\"),\n        selectionchange_handlers: this.updateSelectedHighlight.bind(this),\n        remove_all_formats_handlers: () => {\n            // we rely on the normalize handler to start it again\n            this.dependencies.edit_interaction.stopInteraction(\"website.text_highlight\");\n        },\n        format_selection_handlers: () => {\n            this.dependencies.edit_interaction.stopInteraction(\"website.text_highlight\");\n        },\n        before_save_handlers: () => {\n            this.dependencies.edit_interaction.stopInteraction(\"website.text_highlight\");\n        },\n    };\n\n    setup() {\n        this.previewableApplyHighlight = this.dependencies.history.makePreviewableOperation(\n            this._applyHighlight.bind(this)\n        );\n        this.previewableApplyHighlightStyle = this.dependencies.history.makePreviewableOperation(\n            this._applyHighlightStyle.bind(this)\n        );\n        this.highlightState = reactive({\n            highlightId: undefined,\n            color: \"\",\n            thickness: undefined,\n        });\n    }\n\n    getMaxFontSize() {\n        const nodes = this.dependencies.selection\n            .getTargetedNodes()\n            .map((n) => closestElement(n, \"*\"))\n            .filter(Boolean);\n        const uniqueNodes = new Set(nodes);\n        if (uniqueNodes.size === 0) {\n            uniqueNodes.add(this.document.body);\n        }\n        let max = 0;\n        for (const node of uniqueNodes) {\n            const size = parseFloat(getComputedStyle(node).fontSize);\n            if (size > max) {\n                max = size;\n            }\n        }\n        return max;\n    }\n\n    updateSelectedHighlight() {\n        const nodes = this.getSelectedHighlightNodes();\n        const uniqueNodes = new Set(nodes);\n        if (uniqueNodes.size === 0) {\n            this.highlightState.highlightId = undefined;\n            this.highlightState.color = \"\";\n            this.highlightState.thickness = undefined;\n            return;\n        }\n\n        this.highlightState.highlightId =\n            uniqueNodes.size > 1 ? \"multiple\" : getCurrentTextHighlight(nodes[0]);\n        if (this.highlightState.highlightId) {\n            // If multiple highlights are selected, either show the common highlight properties\n            // or nothing if none\n            const style = nodes.map((node) =>\n                getComputedStyle(node).getPropertyValue(\"--text-highlight-color\")\n            );\n            this.highlightState.color = style.every((v) => v === style[0])\n                ? style[0]\n                : getComputedStyle(this.document.body).getPropertyValue(\"--o-color-1\");\n            const thickness = nodes.map((node) =>\n                getComputedStyle(node).getPropertyValue(\"--text-highlight-width\")\n            );\n            this.highlightState.thickness = thickness.every((v) => v === thickness[0])\n                ? parseInt(thickness[0])\n                : 2;\n        }\n    }\n\n    _applyHighlight(highlightId) {\n        const highlightedNodes = this.getSelectedHighlightNodes();\n        let thicknessToRestore = \"2px\";\n        let colorToRestore = \"var(--o-color-1)\";\n        if (highlightedNodes.length > 0) {\n            const style = getComputedStyle(highlightedNodes[0]);\n            colorToRestore = style.getPropertyValue(\"--text-highlight-color\");\n            thicknessToRestore = style.getPropertyValue(\"--text-highlight-width\");\n        }\n\n        this.dependencies.format.formatSelection(\"highlight\", {\n            formatProps: { highlightId, colorToRestore, thicknessToRestore },\n            applyStyle: true,\n        });\n\n        this.updateSelectedHighlight();\n    }\n\n    applyHighlight(highlightId) {\n        this.previewableApplyHighlight.commit(highlightId);\n    }\n    previewHighlight(highlightId) {\n        this.previewableApplyHighlight.preview(highlightId);\n    }\n    revertHighlight() {\n        this.previewableApplyHighlight.revert();\n    }\n\n    _applyHighlightStyle(style, value) {\n        const highlightedNodes = this.getSelectedHighlightNodes();\n        for (const node of new Set(highlightedNodes)) {\n            node.style.setProperty(style, value);\n        }\n        this.updateSelectedHighlight();\n    }\n\n    applyHighlightStyle(style, value) {\n        this.previewableApplyHighlightStyle.commit(style, value);\n    }\n    previewHighlightStyle(style, value) {\n        this.previewableApplyHighlightStyle.preview(style, value);\n    }\n    revertHighlightStyle() {\n        this.previewableApplyHighlightStyle.revert();\n    }\n    getUsedCustomColors() {\n        const highlights = this.editable.querySelectorAll(\".o_text_highlight\");\n        const usedCustomColors = new Set();\n        for (const highlight of highlights) {\n            const style = getComputedStyle(highlight);\n            const color = style.getPropertyValue(\"--text-highlight-color\");\n            if (isCSSColor(color)) {\n                usedCustomColors.add(rgbaToHex(color).toLowerCase());\n            }\n        }\n        return usedCustomColors;\n    }\n\n    getSelectedHighlightNodes() {\n        return this.dependencies.selection\n            .getTargetedNodes()\n            .map((n) => closestElement(n, \".o_text_highlight\"))\n            .filter(Boolean);\n    }\n    /**\n     * This method completes the selection by ensuring that the selection\n     * always cover all the text nodes within the highlighted elements.\n     */\n    completeHighlightSelection() {\n        const targetedNodes = this.dependencies.selection\n            .getTargetedNodes()\n            .map(\n                (n) =>\n                    closestElement(n, \".o_text_highlight\") ||\n                    n?.querySelector?.(\".o_text_highlight\")\n            );\n        let { startContainer, startOffset, endContainer, endOffset, direction } =\n            this.dependencies.selection.getEditableSelection();\n\n        if (targetedNodes.length > 0) {\n            if (targetedNodes[0]?.matches?.(\".o_text_highlight\")) {\n                const firstTextNode = descendants(targetedNodes[0]).filter(isTextNode)[0];\n                startContainer = firstTextNode;\n                startOffset = 0;\n            }\n            if (targetedNodes.at(-1)?.matches?.(\".o_text_highlight\")) {\n                const lastTextNode = descendants(targetedNodes.at(-1)).filter(isTextNode).at(-1);\n                endContainer = lastTextNode;\n                endOffset = nodeSize(endContainer);\n            }\n        }\n        const [anchorNode, anchorOffset, focusNode, focusOffset] = direction\n            ? [startContainer, startOffset, endContainer, endOffset]\n            : [endContainer, endOffset, startContainer, startOffset];\n        this.dependencies.selection.setSelection({\n            anchorNode,\n            anchorOffset,\n            focusNode,\n            focusOffset,\n        });\n        this.dependencies.selection.focusEditable();\n        this.dependencies.history.stageSelection();\n    }\n\n    deleteSelectedHighlight() {\n        this.dependencies.format.formatSelection(\"highlight\", { applyStyle: false });\n        this.updateSelectedHighlight();\n    }\n}\nregistry.category(\"website-plugins\").add(HighlightPlugin.id, HighlightPlugin);\n\n// Todo: formatsSpecs should allow to be register new formats through resources.\nformatsSpecs.highlight = {\n    isFormatted: (node) => closestElement(node)?.classList.contains(\"o_text_highlight\"),\n    hasStyle: (node) => closestElement(node)?.classList.contains(\"o_text_highlight\"),\n    addStyle: (node, { highlightId, thicknessToRestore, colorToRestore }) => {\n        const styledNode = closestElement(node, \".o_text_highlight\");\n        if (styledNode) {\n            formatsSpecs.highlight.removeStyle(styledNode);\n            node = styledNode;\n        }\n        node.classList.add(\"o_text_highlight\", `o_text_highlight_${highlightId}`);\n        if (colorToRestore && colorToRestore !== \"currentColor\") {\n            node.style.setProperty(\"--text-highlight-color\", colorToRestore);\n        }\n        if (thicknessToRestore) {\n            node.style.setProperty(\"--text-highlight-width\", thicknessToRestore);\n        } else {\n            const style = getComputedStyle(node);\n            node.style.setProperty(\n                \"--text-highlight-width\",\n                Math.round(parseFloat(style.fontSize) * 0.1) + \"px\"\n            );\n        }\n    },\n    removeStyle: (node) => {\n        removeClass(\n            node,\n            ...[...node.classList].filter((cls) => cls.startsWith(\"o_text_highlight\"))\n        );\n        removeStyle(node, \"--text-highlight-width\");\n        removeStyle(node, \"--text-highlight-color\");\n    },\n};\n\nclass HighlightToolbarButton extends Component {\n    static props = {\n        ...toolbarButtonProps,\n        highlightConfiguratorProps: Object,\n        onClick: Function,\n        title: String,\n        getSelection: Function,\n    };\n    static template = xml`\n        <button t-ref=\"root\" t-attf-class=\"btn btn-light o-select-highlight {{highlightState.highlightId ? 'active' : ''}}\" t-on-click=\"openHighlightConfigurator\" t-att-title=\"props.title\">\n            <i class=\"fa oi oi-text-effect oi-fw py-1\"/>\n        </button>\n    `;\n\n    setup() {\n        this.highlightState = useState(this.props.highlightConfiguratorProps.getHighlightState());\n        this.root = useRef(\"root\");\n        this.componentStack = useStackingComponentState();\n        this.componentStack.push(HighlightConfigurator, {\n            componentStack: this.componentStack,\n            ...this.props.highlightConfiguratorProps,\n        });\n        this.configuratorPopover = usePopover(StackingComponent, {\n            env: this.__owl__.childEnv,\n            onClose: () => {\n                while (this.componentStack.stack.length > 1) {\n                    this.componentStack.pop();\n                }\n            },\n        });\n    }\n    openHighlightConfigurator() {\n        this.props.onClick();\n        this.configuratorPopover.open(this.root.el, {\n            stackState: this.componentStack,\n            style: \"max-height: 300px; width: 262px\",\n            class: \"d-flex flex-column p-2\",\n        });\n    }\n}\n", "import { xml, Component, reactive, useState, useEffect } from \"@odoo/owl\";\nimport { POSITION_BUS } from \"@web/core/position/position_hook\";\n\nexport function useStackingComponentState() {\n    const stack = reactive([]);\n    let counter = 0;\n    const push = (component, props, title, withPrevious) => {\n        stack.push({ id: counter++, component, props, title, withPrevious });\n    };\n    const pop = () => stack.pop();\n\n    return { push, pop, stack };\n}\n\nexport class StackingComponent extends Component {\n    static template = xml`\n        <t t-foreach=\"this.stack\" t-as=\"componentSpec\" t-key=\"componentSpec.id\">\n            <div data-prevent-closing-overlay=\"true\" t-if=\"componentSpec_last\" t-attf-class=\"{{this.props.class}} {{componentSpec_last ? '': 'd-none' }}\" t-att-style=\"this.props.style\">\n                <div t-if=\"this.stack.length > 1 || componentSpec.title\" class=\"d-flex align-items-center\">\n                    <button t-if=\"this.stack.length > 1 and componentSpec.withPrevious\" class=\"fa fa-angle-left btn btn-secondary bg-transparent border-0\" t-on-click=\"this.props.stackState.pop\"></button>\n                    <span t-out=\"componentSpec.title\" t-att-class=\"{ 'cursor-pointer': componentSpec.withPrevious }\" t-on-click=\"this.props.stackState.pop\"/>\n                </div>\n                <t t-component=\"componentSpec.component\" t-props=\"componentSpec.props\" />\n            </div>\n        </t>\n    `;\n    static props = {\n        stackState: { type: Object, required: true },\n        class: { type: String, optional: true },\n        style: { type: String, optional: true },\n        close: { type: Function, optional: true },\n    };\n    setup() {\n        this.stack = useState(this.props.stackState.stack);\n        useEffect(\n            () => {\n                // Recompute the positioning of the popover if any.\n                this.env[POSITION_BUS]?.trigger(\"update\");\n            },\n            () => [this.stack.length]\n        );\n    }\n}\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\n\nexport class GridImageOption extends BaseOptionComponent {\n    static template = \"website.GridImageOption\";\n    static selector = \"img\";\n\n    setup() {\n        super.setup();\n        this.state = useDomState((editingElement) => ({\n            isOptionActive: this.isOptionActive(editingElement),\n        }));\n    }\n\n    isOptionActive(editingElement) {\n        const imageGridItemEl = editingElement.closest(\".o_grid_item_image\");\n        // Special conditions for the hover effects.\n        const hasSquareShape = editingElement.dataset.shape === \"html_builder/geometric/geo_square\";\n        const effectAllowsOption = ![\"dolly_zoom\", \"outline\", \"image_mirror_blur\"].includes(\n            editingElement.dataset.hoverEffect\n        );\n\n        return (\n            !!imageGridItemEl &&\n            (!(\"shape\" in editingElement.dataset) || (hasSquareShape && effectAllowsOption))\n        );\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { GRID_IMAGE } from \"@website/builder/option_sequence\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { GridImageOption } from \"./grid_image_option\";\n\nclass GridImageOptionPlugin extends Plugin {\n    static id = \"gridImageOption\";\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(GRID_IMAGE, GridImageOption)],\n        builder_actions: {\n            SetGridImageModeAction,\n        },\n    };\n}\n\nexport class SetGridImageModeAction extends BuilderAction {\n    static id = \"setGridImageMode\";\n    apply({ editingElement, value: mode }) {\n        const imageGridItemEl = editingElement.closest(\".o_grid_item_image\");\n        if (imageGridItemEl) {\n            imageGridItemEl.classList.toggle(\"o_grid_item_image_contain\", mode === \"contain\");\n        }\n    }\n    isApplied({ editingElement, value: mode }) {\n        const imageGridItemEl = editingElement.closest(\".o_grid_item_image\");\n        return imageGridItemEl && imageGridItemEl.classList.contains(\"o_grid_item_image_contain\")\n            ? mode === \"contain\"\n            : mode === \"cover\";\n    }\n}\n\nregistry.category(\"website-plugins\").add(GridImageOptionPlugin.id, GridImageOptionPlugin);\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { convertCSSColorToRgba } from \"@web/core/utils/colors\";\n\n/**\n * @typedef { Object } ImageHoverShared\n * @property { ImageHoverPlugin['setHoverEffect'] } setHoverEffect\n * @property { ImageHoverPlugin['removeHoverEffect'] } removeHoverEffect\n */\n\nexport class ImageHoverPlugin extends Plugin {\n    static id = \"imageHover\";\n    static shared = [\"setHoverEffect\", \"removeHoverEffect\"];\n    static dependencies = [\"imagePostProcess\", \"imageToolOption\"];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_actions: {\n            SetHoverEffectAction,\n            SetHoverEffectIntensityAction,\n            SetHoverEffectColorAction,\n            SetHoverEffectStrokeWidthAction,\n        },\n        system_attributes: [\"data-original-src-before-hover\"],\n        default_shape_handlers: (dataset) =>\n            dataset.hoverEffect && \"html_builder/geometric/geo_square\",\n        post_compute_shape_listeners: async (svg, params) => {\n            let rgba = null;\n            let rbg = null;\n            let opacity = null;\n            // Add the required parts for the hover effects to the SVG.\n            const hoverEffectName = params.hoverEffect;\n            const hoverEffectsSvg = await this.getSvgHoverEffects();\n            const hoverEffectEls = hoverEffectsSvg.querySelectorAll(`#${hoverEffectName} > *`);\n            hoverEffectEls.forEach((hoverEffectEl) => {\n                svg.appendChild(hoverEffectEl.cloneNode(true));\n            });\n            // Modifies the svg according to the chosen hover effect and the value\n            // of the options.\n            const animateEl = svg.querySelector(\"animate\");\n            const animateTransformEls = svg.querySelectorAll(\"animateTransform\");\n            const animateElValues = animateEl?.getAttribute(\"values\");\n            let animateTransformElValues = animateTransformEls[0]?.getAttribute(\"values\");\n            if (params.hoverEffectColor) {\n                rgba = convertCSSColorToRgba(params.hoverEffectColor);\n                rbg = `rgb(${rgba.red},${rgba.green},${rgba.blue})`;\n                opacity = rgba.opacity / 100;\n                if (![\"outline\", \"image_mirror_blur\"].includes(hoverEffectName)) {\n                    svg.querySelector('[fill=\"hover_effect_color\"]').setAttribute(\"fill\", rbg);\n                    animateEl.setAttribute(\n                        \"values\",\n                        animateElValues.replace(\"hover_effect_opacity\", opacity)\n                    );\n                }\n            }\n            switch (hoverEffectName) {\n                case \"outline\": {\n                    svg.querySelector('[stroke=\"hover_effect_color\"]').setAttribute(\"stroke\", rbg);\n                    svg.querySelector('[stroke-opacity=\"hover_effect_opacity\"]').setAttribute(\n                        \"stroke-opacity\",\n                        opacity\n                    );\n                    // The stroke width needs to be multiplied by two because half\n                    // of the stroke is invisible since it is centered on the path.\n                    const strokeWidth = parseInt(params.hoverEffectStrokeWidth) * 2;\n                    animateEl.setAttribute(\n                        \"values\",\n                        animateElValues.replace(\"hover_effect_stroke_width\", strokeWidth)\n                    );\n                    break;\n                }\n                case \"image_zoom_in\":\n                case \"image_zoom_out\":\n                case \"dolly_zoom\": {\n                    const imageEl = svg.querySelector(\"image\");\n                    const clipPathEl = svg.querySelector(\"#clip-path\");\n                    imageEl.setAttribute(\"id\", \"shapeImage\");\n                    // Modify the SVG so that the clip-path is not zoomed when the\n                    // image is zoomed.\n                    imageEl.setAttribute(\n                        \"style\",\n                        \"transform-origin: center; width: 100%; height: 100%\"\n                    );\n                    imageEl.setAttribute(\"preserveAspectRatio\", \"none\");\n                    svg.setAttribute(\"viewBox\", \"0 0 1 1\");\n                    svg.setAttribute(\"preserveAspectRatio\", \"none\");\n                    clipPathEl.setAttribute(\"clipPathUnits\", \"userSpaceOnUse\");\n                    const clipPathValue = imageEl.getAttribute(\"clip-path\");\n                    imageEl.removeAttribute(\"clip-path\");\n                    const gEl = document.createElementNS(\"http://www.w3.org/2000/svg\", \"g\");\n                    gEl.setAttribute(\"clip-path\", clipPathValue);\n                    imageEl.parentNode.replaceChild(gEl, imageEl);\n                    gEl.appendChild(imageEl);\n                    let zoomValue = 1.01 + parseInt(params.hoverEffectIntensity) / 200;\n                    animateTransformEls[0].setAttribute(\n                        \"values\",\n                        animateTransformElValues.replace(\"hover_effect_zoom\", zoomValue)\n                    );\n                    if (hoverEffectName === \"image_zoom_out\") {\n                        // Set zoom intensity for the image.\n                        const styleAttr = svg.querySelector(\"style\");\n                        styleAttr.textContent = styleAttr.textContent.replace(\n                            \"hover_effect_zoom\",\n                            zoomValue\n                        );\n                    }\n                    if (hoverEffectName === \"dolly_zoom\") {\n                        clipPathEl.setAttribute(\"style\", \"transform-origin: center;\");\n                        // Set zoom intensity for clip-path and overlay.\n                        zoomValue = 0.99 - parseInt(params.hoverEffectIntensity) / 2000;\n                        animateTransformEls.forEach((animateTransformEl, index) => {\n                            if (index > 0) {\n                                animateTransformElValues =\n                                    animateTransformEl.getAttribute(\"values\");\n                                animateTransformEl.setAttribute(\n                                    \"values\",\n                                    animateTransformElValues.replace(\"hover_effect_zoom\", zoomValue)\n                                );\n                            }\n                        });\n                    }\n                    break;\n                }\n                case \"image_mirror_blur\": {\n                    const imageEl = svg.querySelector(\"image\");\n                    imageEl.setAttribute(\"id\", \"shapeImage\");\n                    imageEl.setAttribute(\"style\", \"transform-origin: center;\");\n                    const imageMirrorEl = imageEl.cloneNode();\n                    imageMirrorEl.setAttribute(\"id\", \"shapeImageMirror\");\n                    imageMirrorEl.setAttribute(\"filter\", \"url(#blurFilter)\");\n                    imageEl.insertAdjacentElement(\"beforebegin\", imageMirrorEl);\n                    const zoomValue = 0.99 - parseInt(params.hoverEffectIntensity) / 200;\n                    animateTransformEls[0].setAttribute(\n                        \"values\",\n                        animateTransformElValues.replace(\"hover_effect_zoom\", zoomValue)\n                    );\n                    break;\n                }\n            }\n        },\n        remove_hover_effect_handlers: this.removeHoverEffect.bind(this),\n        set_hover_effect_handlers: this.setHoverEffect.bind(this),\n    };\n\n    defaultHoverEffectIntensity = 20;\n\n    async setHoverEffect(imgEl, hoverEffectId = \"overlay\") {\n        const updateAttributes = await this.dependencies.imagePostProcess.processImage({\n            img: imgEl,\n            newDataset: this.getDefaultValue(hoverEffectId),\n        });\n        updateAttributes();\n    }\n\n    async removeHoverEffect(imgEl) {\n        const updateAttributes = await this.dependencies.imagePostProcess.processImage({\n            img: imgEl,\n            newDataset: {\n                hoverEffect: undefined,\n                hoverEffectColor: undefined,\n                hoverEffectStrokeWidth: undefined,\n                hoverEffectIntensity: undefined,\n            },\n        });\n        updateAttributes();\n    }\n    /**\n     * Gets the hover effects list.\n     *\n     * @private\n     * @returns {Promise<SVGElement>}\n     */\n    async getSvgHoverEffects() {\n        if (this.hoverEffectsSvg) {\n            return this.hoverEffectsSvg;\n        }\n        const hoverEffectsURL = \"/website/static/src/svg/hover_effects.svg\";\n        const text = await fetch(hoverEffectsURL).then((r) => r.text());\n        const parser = new DOMParser();\n        const xmlDoc = parser.parseFromString(text, \"text/xml\");\n        this.hoverEffectsSvg = xmlDoc.getElementsByTagName(\"svg\")[0];\n        return this.hoverEffectsSvg;\n    }\n    getDefaultValue(hoverEffectId) {\n        const defaultColor = { hoverEffectColor: \"rgba(0, 0, 0, 0)\" };\n        const defaultEffectValues = {\n            overlay: () => ({\n                hoverEffectColor: this.dependencies.imageToolOption.getCSSColorValue(\"black-25\"),\n            }),\n            outline: () => ({\n                hoverEffectColor: this.dependencies.imageToolOption.getCSSColorValue(\"primary\"),\n                hoverEffectStrokeWidth: 10,\n            }),\n            image_zoom_in: () => defaultColor,\n            image_zoom_out: () => defaultColor,\n            dolly_zoom: () => defaultColor,\n        };\n\n        return {\n            hoverEffectIntensity: String(this.defaultHoverEffectIntensity),\n            ...defaultEffectValues[hoverEffectId]?.(),\n            hoverEffect: hoverEffectId,\n        };\n    }\n}\nexport class SetHoverEffectAction extends BuilderAction {\n    static id = \"setHoverEffect\";\n    static dependencies = [\"imageHover\"];\n\n    isApplied({ editingElement, value: hoverEffectId }) {\n        return editingElement.dataset.hoverEffect === hoverEffectId;\n    }\n    async apply({ editingElement, value: hoverEffectId, isPreviewing }) {\n        await this.dependencies.imageHover.setHoverEffect(editingElement, hoverEffectId);\n        if (isPreviewing) {\n            // Wait a tick to ensure the interactions are restarted.\n            // Simulate a mouseenter event to trigger the hover effect. (See\n            // `ImageShapeHoverEffect`).\n            setTimeout(() => {\n                editingElement.dispatchEvent(new Event(\"mouseenter\"));\n            });\n        }\n    }\n}\nexport class SetHoverEffectIntensityAction extends BuilderAction {\n    static id = \"setHoverEffectIntensity\";\n    static dependencies = [\"imagePostProcess\"];\n\n    getValue({ editingElement }) {\n        return parseInt(\n            editingElement.dataset.hoverEffectIntensity || this.defaultHoverEffectIntensity,\n            10\n        );\n    }\n    async apply({ editingElement, value: intensity }) {\n        const updateAttributes = await this.dependencies.imagePostProcess.processImage({\n            img: editingElement,\n            newDataset: {\n                hoverEffectIntensity: String(intensity),\n            },\n        });\n        updateAttributes();\n    }\n}\nexport class SetHoverEffectColorAction extends BuilderAction {\n    static id = \"setHoverEffectColor\";\n    static dependencies = [\"imagePostProcess\"];\n\n    getValue({ editingElement }) {\n        return editingElement.dataset.hoverEffectColor;\n    }\n    async apply({ editingElement, value: color }) {\n        const updateAttributes = await this.dependencies.imagePostProcess.processImage({\n            img: editingElement,\n            newDataset: {\n                hoverEffectColor: color,\n            },\n        });\n        updateAttributes();\n    }\n}\nexport class SetHoverEffectStrokeWidthAction extends BuilderAction {\n    static id = \"setHoverEffectStrokeWidth\";\n    static dependencies = [\"imagePostProcess\"];\n\n    getValue({ editingElement }) {\n        return editingElement.dataset.hoverEffectStrokeWidth\n            ? parseInt(editingElement.dataset.hoverEffectStrokeWidth, 10)\n            : undefined;\n    }\n    async apply({ editingElement, value: strokeWidth }) {\n        const updateAttributes = await this.dependencies.imagePostProcess.processImage({\n            img: editingElement,\n            newDataset: {\n                hoverEffectStrokeWidth: String(strokeWidth),\n            },\n        });\n        updateAttributes();\n    }\n}\nregistry.category(\"website-plugins\").add(ImageHoverPlugin.id, ImageHoverPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class AddElementOption extends BaseOptionComponent {\n    static template = \"website.AddElementOption\";\n    static props = {\n        level: { type: Number, optional: true },\n        applyTo: { type: String, optional: true },\n    };\n    static defaultProps = {\n        level: 0,\n    };\n}\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { resizeGrid, setElementToMaxZindex } from \"@html_builder/utils/grid_layout_utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { onceAllImagesLoaded } from \"@website/utils/images\";\n\n/**\n * @typedef { Object } AddElementOptionShared\n * @property { AddElementOptionPlugin['addGridElement'] } addGridElement\n */\n\nexport class AddElementOptionPlugin extends Plugin {\n    static id = \"addElementOption\";\n    static dependencies = [\"builderOptions\"];\n    static shared = [\"addGridElement\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_actions: {\n            AddGridElementAction,\n        },\n    };\n\n    /**\n     * Adds a new grid item in the grid with the given content and properties.\n     *\n     * @param {HTMLElement} rowEl the grid\n     * @param {HTMLElement} contentEl the content to add in the column\n     * @param {Number} columnSpan the grid item column span\n     * @param {Number} rowSpan the grid item row span\n     * @param {Array<String>} [extraClasses = []] classes to add to the grid\n     *     item\n     */\n    addGridElement(rowEl, contentEl, columnSpan, rowSpan, extraClasses = []) {\n        // If it has been less than 15 seconds that we have added an element,\n        // shift the new element right and down by one cell. Otherwise, put it\n        // in the top left corner.\n        const currentTime = new Date().getTime();\n        if (this.lastAddTime && (currentTime - this.lastAddTime) / 1000 < 15) {\n            this.lastStartPosition = [this.lastStartPosition[0] + 1, this.lastStartPosition[1] + 1];\n        } else {\n            this.lastStartPosition = [1, 1]; // [rowStart, columnStart]\n        }\n        this.lastAddTime = currentTime;\n\n        // Create the new column.\n        const newColumnEl = document.createElement(\"div\");\n        newColumnEl.classList.add(\"o_grid_item\", ...extraClasses);\n        newColumnEl.classList.add(\n            `g-col-lg-${columnSpan}`,\n            `col-lg-${columnSpan}`,\n            `g-height-${rowSpan}`\n        );\n        newColumnEl.appendChild(contentEl);\n\n        // Place the column in the grid.\n        const rowStart = this.lastStartPosition[0];\n        let columnStart = this.lastStartPosition[1];\n        if (columnStart + columnSpan > 13) {\n            columnStart = 1;\n            this.lastStartPosition[1] = columnStart;\n        }\n        newColumnEl.style.gridArea = `\n            ${rowStart} / ${columnStart} / ${rowStart + rowSpan} / ${columnStart + columnSpan}\n        `;\n\n        // Set the z-index to the maximum of the grid.\n        setElementToMaxZindex(newColumnEl, rowEl);\n\n        // Add the new column and update the grid height.\n        rowEl.appendChild(newColumnEl);\n        resizeGrid(rowEl);\n\n        // Scroll to the new column if more than half of it is hidden (= out of\n        // the viewport or hidden by an other element).\n        const newColumnPosition = newColumnEl.getBoundingClientRect();\n        const middleX = (newColumnPosition.left + newColumnPosition.right) / 2;\n        const middleY = (newColumnPosition.top + newColumnPosition.bottom) / 2;\n        const sameCoordinatesEl = this.document.elementFromPoint(middleX, middleY);\n        if (!sameCoordinatesEl || !newColumnEl.contains(sameCoordinatesEl)) {\n            newColumnEl.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n        }\n        // Activate the new column options.\n        this.dependencies.builderOptions.setNextTarget(newColumnEl);\n    }\n}\n\n/**\n * Adds an image, some text or a button in the grid.\n */\nexport class AddGridElementAction extends BuilderAction {\n    static id = \"addGridElement\";\n    static dependencies = [\"addElementOption\", \"media\"];\n\n    async apply({ editingElement: rowEl, params: { mainParam: elementType } }) {\n        if (elementType === \"image\") {\n            // Choose an image with the media dialog.\n            let imageEl;\n            await this.dependencies.media.openMediaDialog({\n                onlyImages: true,\n                noDocuments: true,\n                save: (selectedImageEl) => (imageEl = selectedImageEl),\n            });\n            if (!imageEl) {\n                return;\n            }\n            // Wait for the image to be loaded.\n            await onceAllImagesLoaded(imageEl);\n            this.dependencies.addElementOption.addGridElement(rowEl, imageEl, 6, 6, [\n                \"o_grid_item_image\",\n            ]);\n        } else if (elementType === \"text\") {\n            // Create default text content.\n            const pEl = document.createElement(\"p\");\n            pEl.textContent = _t(\"Write something...\");\n            this.dependencies.addElementOption.addGridElement(rowEl, pEl, 4, 2);\n        } else if (elementType === \"button\") {\n            // Create default button.\n            const aEl = document.createElement(\"a\");\n            aEl.href = \"#\";\n            aEl.classList.add(\"mb-2\", \"btn\", \"btn-primary\");\n            aEl.textContent = _t(\"Button\");\n            this.dependencies.addElementOption.addGridElement(rowEl, aEl, 2, 1);\n        }\n    }\n}\n\nregistry.category(\"website-plugins\").add(AddElementOptionPlugin.id, AddElementOptionPlugin);\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\n\nexport class GridColumnsOption extends BaseOptionComponent {\n    static template = \"website.GridColumnsOption\";\n    static selector = \".row:not(.s_col_no_resize) > div\";\n\n    setup() {\n        super.setup();\n        this.state = useDomState((editingElement) => ({\n            isGridMode: editingElement.parentElement.classList.contains(\"o_grid_mode\"),\n        }));\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { GridColumnsOption } from \"./grid_column_option\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { GRID_COLUMNS } from \"@website/builder/option_sequence\";\nimport { StyleAction } from \"@html_builder/core/core_builder_action_plugin\";\n\nexport class GridColumnsOptionPlugin extends Plugin {\n    static id = \"GridColumnsOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(GRID_COLUMNS, GridColumnsOption)],\n        builder_actions: {\n            SetGridColumnsPaddingAction,\n        },\n        system_classes: [\"o_we_padding_highlight\"],\n    };\n}\n\nregistry.category(\"website-plugins\").add(GridColumnsOptionPlugin.id, GridColumnsOptionPlugin);\n\nconst removePaddingPreview = (event) => {\n    const editingElement = event.target;\n    editingElement.classList.remove(\"o_we_padding_highlight\");\n    editingElement.removeEventListener(\"animationend\", removePaddingPreview);\n};\nexport class SetGridColumnsPaddingAction extends StyleAction {\n    static id = \"setGridColumnsPadding\";\n    apply(...args) {\n        const { editingElement } = args[0];\n        removePaddingPreview({ target: editingElement });\n        super.apply(...args);\n        editingElement.classList.add(\"o_we_padding_highlight\");\n        editingElement.addEventListener(\"animationend\", removePaddingPreview);\n    }\n}\n", "import { SelectNumberColumn } from \"@html_builder/core/select_number_column\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { AddElementOption } from \"./add_element_option\";\nimport { SpacingOption } from \"./spacing_option\";\n\nexport class LayoutOption extends BaseOptionComponent {\n    static template = \"website.LayoutOption\";\n    static selector = \"section, section.s_carousel_wrapper .carousel-item, .s_carousel_intro_item\";\n    static exclude =\n        \".s_dynamic, .s_dynamic_snippet_content, .s_dynamic_snippet_title, .s_masonry_block, .s_framed_intro, .s_features_grid, .s_media_list, .s_table_of_content, .s_process_steps, .s_image_gallery, .s_pricelist_boxed, .s_quadrant, .s_pricelist_cafe, .s_faq_horizontal, .s_image_frame, .s_card_offset, .s_contact_info, .s_tabs, .s_tabs_images, .s_floating_blocks .s_floating_blocks_block, .s_banner_categories\";\n    static applyTo = \":scope > *:has(> .row), :scope > .s_allow_columns\";\n    static components = {\n        SelectNumberColumn,\n        SpacingOption,\n        AddElementOption,\n    };\n}\n\nexport class LayoutGridOption extends BaseOptionComponent {\n    static template = \"website.LayoutGridOption\";\n    static selector =\n        \"section.s_masonry_block, section.s_quadrant, section.s_image_frame, section.s_card_offset, section.s_contact_info, section.s_framed_intro, section.s_banner_categories\";\n    static applyTo = \":scope > *:has(> .row)\";\n    static components = {\n        SpacingOption,\n        AddElementOption,\n    };\n}\n", "import { getRow } from \"@html_builder/utils/column_layout_utils\";\nimport {\n    convertToNormalColumn,\n    reloadLazyImages,\n    toggleGridMode,\n} from \"@html_builder/utils/grid_layout_utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { LayoutGridOption, LayoutOption } from \"./layout_option\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { LAYOUT, LAYOUT_GRID } from \"@website/builder/option_sequence\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\n\nclass LayoutOptionPlugin extends Plugin {\n    static id = \"LayoutOption\";\n    static dependencies = [\"clone\", \"selection\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(LAYOUT, LayoutOption),\n            withSequence(LAYOUT_GRID, LayoutGridOption),\n        ],\n        on_cloned_handlers: this.onCloned.bind(this),\n        builder_actions: {\n            SetGridLayoutAction,\n            SetColumnLayoutAction,\n        },\n    };\n    onCloned({ cloneEl }) {\n        const cloneElClassList = cloneEl.classList;\n        const offsetClasses = [...cloneElClassList].filter((cls) =>\n            cls.match(/^offset-(lg-)?([0-9]{1,2})$/)\n        );\n        cloneElClassList.remove(...offsetClasses);\n    }\n}\n\nconst isGrid = (el) => {\n    const rowEl = getRow(el);\n    return !!(rowEl && rowEl.classList.contains(\"o_grid_mode\"));\n};\nexport class SetGridLayoutAction extends BuilderAction {\n    static id = \"setGridLayout\";\n    static dependencies = [\"selection\"];\n    apply({ editingElement }) {\n        // TODO no preview/apply if it s isApplied\n        if (isGrid(editingElement)) {\n            return;\n        }\n        toggleGridMode(\n            editingElement,\n            this.dependencies.selection.preserveSelection,\n            this.config.mobileBreakpoint\n        );\n    }\n    isApplied({ editingElement }) {\n        return isGrid(editingElement);\n    }\n}\nexport class SetColumnLayoutAction extends BuilderAction {\n    static id = \"setColumnLayout\";\n    apply({ editingElement }) {\n        const rowEl = getRow(editingElement);\n        // TODO no preview/apply if it s isApplied\n        if (!isGrid(editingElement)) {\n            return;\n        }\n\n        // Removing the grid class\n        rowEl.classList.remove(\"o_grid_mode\");\n        const columnEls = rowEl.children;\n\n        for (const columnEl of columnEls) {\n            // Reloading the images.\n            reloadLazyImages(columnEl);\n            // Removing the grid properties.\n            convertToNormalColumn(columnEl, this.config.mobileBreakpoint);\n        }\n        // Removing the grid properties.\n        delete rowEl.dataset.rowCount;\n        // Kept for compatibility.\n        rowEl.style.removeProperty(\"--grid-item-padding-x\");\n        rowEl.style.removeProperty(\"--grid-item-padding-y\");\n        rowEl.style.removeProperty(\"gap\");\n    }\n    isApplied({ editingElement }) {\n        return !isGrid(editingElement);\n    }\n}\nregistry.category(\"website-plugins\").add(LayoutOptionPlugin.id, LayoutOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class SpacingOption extends BaseOptionComponent {\n    static template = \"website.SpacingOption\";\n    static props = {\n        level: { type: Number, optional: true },\n        applyTo: { type: String, optional: true },\n    };\n    static defaultProps = {\n        level: 0,\n    };\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { isBlock } from \"@html_editor/utils/blocks\";\nimport { registry } from \"@web/core/registry\";\nimport { addBackgroundGrid, setElementToMaxZindex } from \"@html_builder/utils/grid_layout_utils\";\nimport { StyleAction } from \"@html_builder/core/core_builder_action_plugin\";\n\nclass SpacingOptionPlugin extends Plugin {\n    static id = \"SpacingOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_actions: {\n            SetGridSpacingAction,\n        },\n        savable_mutation_record_predicates: this.isMutationRecordSavable.bind(this),\n        on_cloned_handlers: this.onCloned.bind(this),\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n    };\n\n    /**\n     * @param {import(\"@html_editor/core/history_plugin\").HistoryMutationRecord} record\n     */\n    isMutationRecordSavable(record) {\n        // Do not consider the grid preview in the history.\n        if (record.type === \"childList\") {\n            const node = (record.addedTrees[0] || record.removedTrees[0]).node;\n            if (node.matches && node.matches(\".o_we_grid_preview\") && isBlock(node)) {\n                return false;\n            }\n        }\n        if (record.type === \"attributes\") {\n            if (record.target.matches(\".o_we_grid_preview\")) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    removeGridPreviews(el) {\n        el.querySelectorAll(\".o_we_grid_preview\").forEach((gridPreviewEl) =>\n            gridPreviewEl.remove()\n        );\n    }\n\n    onCloned({ cloneEl }) {\n        this.removeGridPreviews(cloneEl);\n    }\n\n    cleanForSave({ root }) {\n        this.removeGridPreviews(root);\n    }\n}\n\nregistry.category(\"website-plugins\").add(SpacingOptionPlugin.id, SpacingOptionPlugin);\n\nexport class SetGridSpacingAction extends StyleAction {\n    static id = \"setGridSpacing\";\n    apply({ editingElement: rowEl }) {\n        // Remove the grid preview if any.\n        let gridPreviewEl = rowEl.querySelector(\".o_we_grid_preview\");\n        if (gridPreviewEl) {\n            gridPreviewEl.remove();\n        }\n        // Apply the style action on the grid gaps.\n        super.apply(...arguments);\n        // Add an animated grid preview.\n        gridPreviewEl = addBackgroundGrid(rowEl, 0);\n        gridPreviewEl.classList.add(\"o_we_grid_preview\");\n        setElementToMaxZindex(gridPreviewEl, rowEl);\n        gridPreviewEl.addEventListener(\"animationend\", () => gridPreviewEl.remove());\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { NavbarLinkPopover } from \"./navbar_link_popover/navbar_link_popover\";\nimport { MenuDialog, EditMenuDialog } from \"@website/components/dialog/edit_menu\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\n/**\n * @typedef { Object } MenuDataShared\n * @property { MenuDataPlugin['openEditMenu'] } openEditMenu\n */\n\nexport class MenuDataPlugin extends Plugin {\n    static id = \"menuDataPlugin\";\n    static shared = [\"openEditMenu\"];\n    static dependencies = [\"savePlugin\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        link_popovers: [\n            withSequence(10, {\n                PopoverClass: NavbarLinkPopover,\n                isAvailable: (linkElement) =>\n                    linkElement &&\n                    linkElement.closest(\".top_menu, o_extra_menu_items, [data-content_menu_id]\") &&\n                    !linkElement.closest(\n                        \".dropdown-toggle, li.o_header_menu_button a, [data-toggle], .o_offcanvas_logo, .o_mega_menu\"\n                    ),\n                getProps: (props) => ({\n                    ...props,\n                    onClickEditLink: (elem, callback) => {\n                        const menuEl = elem.props.linkElement.querySelector(\"[data-oe-id]\");\n                        this.services.dialog.add(MenuDialog, {\n                            name: menuEl.textContent,\n                            url: menuEl.parentElement.attributes[\"href\"].nodeValue,\n                            save: async (name, url) => {\n                                const websiteId = this.services.website.currentWebsite.id;\n                                const data = {\n                                    id: parseInt(menuEl.attributes[\"data-oe-id\"].nodeValue),\n                                    name,\n                                    url,\n                                };\n                                const result = await this.services.orm.call(\n                                    \"website.menu\",\n                                    \"save\",\n                                    [websiteId, { data: [data] }]\n                                );\n                                menuEl.parentElement.attributes[\"href\"].nodeValue = url;\n                                menuEl.textContent = name;\n                                callback();\n                                return result;\n                            },\n                        });\n                    },\n                    onClickEditMenu: this.openEditMenu.bind(this, props.linkElement),\n                }),\n            }),\n        ],\n        is_link_editable_predicates: this.isMenuLink.bind(this),\n    };\n\n    setup() {\n        this.websiteService = this.services.website;\n    }\n\n    openEditMenu(linkEl) {\n        if (this.isEditMenuOpening) {\n            return Promise.resolve();\n        }\n        this.isEditMenuOpening = true;\n        return new Promise((resolve) => {\n            const rootID = parseInt(\n                linkEl?.closest(\"[data-content_menu_id]\")?.dataset.content_menu_id\n            );\n            this.services.dialog.add(\n                EditMenuDialog,\n                {\n                    rootID: isNaN(rootID) ? null : rootID,\n                    save: async (newPageUrl) => {\n                        // Save the page before reloading the editor.\n                        await this.dependencies.savePlugin.save();\n                        await this.config.reloadEditor();\n                        if (newPageUrl) {\n                            this.websiteService.goToWebsite({\n                                path: newPageUrl,\n                                edition: true,\n                                websiteId: this.websiteService.currentWebsite.id,\n                            });\n                        }\n                    },\n                },\n                {\n                    onClose: () => {\n                        this.isEditMenuOpening = false;\n                        resolve();\n                    },\n                }\n            );\n        });\n    }\n\n    /**\n     * This predicate is used to determine if the link element is editable.\n     * It checks if the link element is a menu item or a nav link\n     * @param {HTMLElement} linkElement - The link element to check.\n     * @returns {boolean} - True if the link element is editable, false otherwise.\n     */\n    isMenuLink(linkElement) {\n        return (\n            linkElement &&\n            (linkElement.getAttribute(\"role\") === \"menuitem\" ||\n                linkElement.classList.contains(\"nav-link\")) &&\n            !linkElement.dataset.bsToggle\n        );\n    }\n}\n\nregistry.category(\"website-plugins\").add(MenuDataPlugin.id, MenuDataPlugin);\n", "import { LinkPopover } from \"@html_editor/main/link/link_popover\";\n\nexport class NavbarLinkPopover extends LinkPopover {\n    static template = \"website.navbarLinkPopover\";\n    static props = {\n        ...LinkPopover.props,\n        onClickEditLink: Function,\n        onClickEditMenu: Function,\n    };\n\n    /**\n     * @override\n     */\n    onClickEdit() {\n        const updateUrlAndLabel = this.updateUrlAndLabel.bind(this);\n        const applyDeducedUrl = this.applyDeducedUrl.bind(this);\n        const callback = () => {\n            updateUrlAndLabel();\n            applyDeducedUrl();\n        };\n        this.props.onClickEditLink(this, callback);\n    }\n\n    onClickEditMenu() {\n        this.props.onClickEditMenu();\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { SNIPPET_SPECIFIC } from \"@html_builder/utils/option_sequence\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class AccordionOption extends BaseOptionComponent {\n    static template = \"website.AccordionOption\";\n    static selector = \".s_accordion\";\n}\n\nexport class AccordionItemOption extends BaseOptionComponent {\n    static template = \"website.AccordionItemOption\";\n    static selector = \".s_accordion .accordion-item\";\n}\n\nclass accordionOptionPlugin extends Plugin {\n    static id = \"accordionOptionPlugin\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(SNIPPET_SPECIFIC, AccordionOption),\n            withSequence(SNIPPET_SPECIFIC, AccordionItemOption),\n        ],\n        so_content_addition_selector: [\".s_accordion\"],\n        builder_actions: {\n            DefineCustomIconAction,\n            CustomAccordionIconAction,\n        },\n        content_not_editable_selectors: [\".accordion-button\"],\n        content_editable_selectors: [\".accordion-button span\"],\n    };\n}\n\nexport class DefineCustomIconAction extends BuilderAction {\n    static id = \"defineCustomIcon\";\n    static dependencies = [\"media\"];\n    async load() {\n        let selectedIconClass;\n        await new Promise((resolve) => {\n            const onClose = this.dependencies.media.openMediaDialog({\n                visibleTabs: [\"ICONS\"],\n                save: (icon) => {\n                    selectedIconClass = icon.className;\n                    resolve();\n                },\n            });\n            onClose.then(resolve);\n        });\n        return selectedIconClass;\n    }\n    apply({ editingElement, params, loadResult: customClass }) {\n        if (!customClass) {\n            return;\n        }\n        const isActiveIcon = params.isActiveIcon;\n        const media = document.createElement(\"i\");\n        media.className = isActiveIcon\n            ? editingElement.dataset.activeCustomIcon\n            : editingElement.dataset.inactiveCustomIcon;\n        const activeIconsEls = editingElement.querySelectorAll(\".o_custom_icon_active i\");\n        const inactiveIconsEls = editingElement.querySelectorAll(\".o_custom_icon_inactive i\");\n        const iconsEls = isActiveIcon ? activeIconsEls : inactiveIconsEls;\n        iconsEls.forEach((iconEl) => {\n            iconEl.removeAttribute(\"class\");\n            iconEl.classList.add(...customClass.split(\" \"));\n        });\n        if (iconsEls === activeIconsEls) {\n            editingElement.dataset.activeCustomIcon = customClass;\n        } else {\n            editingElement.dataset.inactiveCustomIcon = customClass;\n        }\n    }\n}\nexport class CustomAccordionIconAction extends BuilderAction {\n    static id = \"customAccordionIcon\";\n    apply({ editingElement, params, value }) {\n        const accordionButtonEls = editingElement.querySelectorAll(\".accordion-button\");\n        const activeCustomIcon = editingElement.dataset.activeCustomIcon || \"fa fa-arrow-up\";\n        const inactiveCustomIcon = editingElement.dataset.inactiveCustomIcon || \"fa fa-arrow-down\";\n        if (value) {\n            if (value === \"custom\") {\n                editingElement.dataset.activeCustomIcon = activeCustomIcon;\n                editingElement.dataset.inactiveCustomIcon = inactiveCustomIcon;\n            }\n            accordionButtonEls.forEach((item) => {\n                let el = item.querySelector(\".o_custom_icons_wrap\");\n                if (!el) {\n                    el = document.createElement(\"span\");\n                    el.className =\n                        \"o_custom_icons_wrap position-relative d-block flex-shrink-0 overflow-hidden\";\n                    item.appendChild(el);\n                }\n\n                while (el.firstChild) {\n                    el.removeChild(el.firstChild);\n                }\n                if (!params.selectIcons) {\n                    return;\n                }\n                const customIconsClasses =\n                    \"position-absolute top-0 end-0 bottom-0 start-0 d-flex align-items-center justify-content-center\";\n                const customIconActiveEl = document.createElement(\"span\");\n                customIconActiveEl.className = customIconsClasses;\n                customIconActiveEl.classList.add(\"o_custom_icon_active\");\n                const customIconActiveIEl = document.createElement(\"i\");\n                customIconActiveIEl.className = activeCustomIcon;\n                customIconActiveEl.appendChild(customIconActiveIEl);\n                el.appendChild(customIconActiveEl);\n                const customIconInactiveEl = document.createElement(\"span\");\n                customIconInactiveEl.className = customIconsClasses;\n                customIconInactiveEl.classList.add(\"o_custom_icon_inactive\");\n                const customIconInactiveIEl = document.createElement(\"i\");\n                customIconInactiveIEl.className = inactiveCustomIcon;\n                customIconInactiveEl.appendChild(customIconInactiveIEl);\n                el.appendChild(customIconInactiveEl);\n            });\n        } else {\n            accordionButtonEls.forEach((item) => {\n                const customIconWrapEl = item.querySelector(\".o_custom_icons_wrap\");\n                if (customIconWrapEl) {\n                    customIconWrapEl.remove();\n                }\n            });\n        }\n        if (value !== \"custom\") {\n            delete editingElement.dataset.activeCustomIcon;\n            delete editingElement.dataset.inactiveCustomIcon;\n        }\n    }\n}\n\nregistry.category(\"website-plugins\").add(accordionOptionPlugin.id, accordionOptionPlugin);\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { isImageSupportedForStyle } from \"@html_builder/plugins/image/replace_media_option\";\n\n/**\n * @typedef {((el: HTMLElement) => Promise<boolean>)[]} hover_effect_allowed_predicates\n */\n\nexport class AnimateOption extends BaseOptionComponent {\n    static template = \"website.AnimateOption\";\n    static dependencies = [\"animateOption\"];\n    static selector = \".o_animable, section .row > div, img, .fa, .btn\";\n    static exclude =\n        \"[data-oe-xpath], .o_not-animable, .s_col_no_resize.row > div, .s_col_no_resize\";\n    static props = {\n        dropdownClass: { type: String, optional: true, default: \"o-hb-select-dropdown\" },\n        requireAnimation: { type: Boolean, optional: true },\n        slots: { type: Object, optional: true },\n    };\n    static defaultProps = { requireAnimation: false };\n\n    setup() {\n        super.setup();\n        this.state = useDomState(async (editingElement) => {\n            const hasAnimateClass = editingElement.classList.contains(\"o_animate\");\n            this.getDirectionsItems = this.dependencies.animateOption.getDirectionsItems;\n            const { getEffectsItems } = this.dependencies.animateOption;\n\n            return {\n                isOptionActive: this.isOptionActive(editingElement),\n                hasAnimateClass: hasAnimateClass,\n                canHover: await this.canHaveHoverEffect(editingElement),\n                isLimitedEffect: this.limitedEffects.some((className) =>\n                    editingElement.classList.contains(className)\n                ),\n                showIntensity: this.shouldShowIntensity(editingElement, hasAnimateClass),\n                effectItems: getEffectsItems(this.isActiveItem),\n                directionItems: this.getDirectionsItems(editingElement).filter(\n                    (i) => !i.check || i.check(editingElement)\n                ),\n                isInDropdown: editingElement.closest(\".dropdown\"),\n            };\n        });\n    }\n    get limitedEffects() {\n        // Animations for which the \"On Scroll\" and \"Direction\" options are not\n        // available.\n        return [\n            \"o_anim_flash\",\n            \"o_anim_pulse\",\n            \"o_anim_shake\",\n            \"o_anim_tada\",\n            \"o_anim_flip_in_x\",\n            \"o_anim_flip_in_y\",\n        ];\n    }\n\n    isOptionActive(editingElement) {\n        if (editingElement.matches(\"img\")) {\n            return isImageSupportedForStyle(editingElement);\n        }\n        return true;\n    }\n\n    shouldShowIntensity(editingElement, hasAnimateClass) {\n        if (!hasAnimateClass) {\n            return false;\n        }\n        if (!editingElement.classList.contains(\"o_anim_fade_in\")) {\n            return true;\n        }\n\n        const possibleDirections = this.getDirectionsItems()\n            .map((i) => i.className)\n            .filter(Boolean);\n        const hasDirection = possibleDirections.some((direction) =>\n            editingElement.classList.contains(direction)\n        );\n\n        return hasDirection;\n    }\n    async canHaveHoverEffect(el) {\n        const proms = this.getResource(\"hover_effect_allowed_predicates\").map((p) => p(el));\n        const settledProms = await Promise.all(proms);\n        return settledProms.length && settledProms.every(Boolean);\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\nimport { getScrollingElement } from \"@web/core/utils/scrolling\";\nimport { AnimateOption } from \"./animate_option\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { AnimateText } from \"./animate_text\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\nimport { ancestors, closestElement, findFurthest } from \"@html_editor/utils/dom_traversal\";\nimport { ANIMATE } from \"@html_builder/utils/option_sequence\";\nimport { childNodeIndex, DIRECTIONS, nodeSize } from \"@html_editor/utils/position\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { EmphasizeAnimatedText } from \"./emphasize_animated_text\";\n\n/**\n * @typedef { Object } AnimateOptionShared\n * @property { AnimateOptionPlugin['forceAnimation'] } forceAnimation\n * @property { AnimateOptionPlugin['getDirectionsItems'] } getDirectionsItems\n * @property { AnimateOptionPlugin['getEffectsItems'] } getEffectsItems\n */\n\n/**\n * @typedef {((editingElement: HTMLElement) => Promise<void>)[]} remove_hover_effect_handlers\n * @typedef {((editingElement: HTMLElement) => Promise<void>)[]} set_hover_effect_handlers\n */\n\nexport class AnimateOptionPlugin extends Plugin {\n    static id = \"animateOption\";\n    static dependencies = [\"history\", \"selection\", \"split\"];\n    static shared = [\"forceAnimation\", \"getDirectionsItems\", \"getEffectsItems\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(ANIMATE, AnimateOption)],\n        toolbar_items: [\n            {\n                id: \"animateText\",\n                groupId: \"websiteDecoration\",\n                description: _t(\"Animate Text\"),\n                Component: AnimateText,\n                props: {\n                    config: this.config.getAnimateTextConfig(),\n                    getAnimatedTextOrCreateDefault: this.getAnimatedTextOrCreateDefault.bind(this),\n                    isActive: this.isAnimatedTextActive.bind(this),\n                    isDisabled: this.isAnimatedTextDisabled.bind(this),\n                    animateOptionProps: { ...this.animateOptionProps, requireAnimation: true },\n                },\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        toolbar_namespace_providers: [\n            withSequence(90, (targetedNodes, editableSelection) =>\n                closestElement(editableSelection.commonAncestorContainer, \".o_animated_text\")\n                    ? \"compact\"\n                    : undefined\n            ),\n        ],\n        system_classes: [\"o_animating\"],\n        builder_actions: {\n            SetAnimationModeAction,\n            SetAnimateIntensityAction,\n            ForceAnimationAction,\n            SetAnimationEffectAction,\n        },\n        normalize_handlers: this.normalize.bind(this),\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        unsplittable_node_predicates: (node) => node.classList?.contains(\"o_animated_text\"),\n        lower_panel_entries: withSequence(10, { Component: EmphasizeAnimatedText }),\n    };\n\n    setup() {\n        this.scrollingElement = getScrollingElement(this.document);\n    }\n\n    getEffectsItems(isActiveItem) {\n        const isOnAppearance = () => isActiveItem(\"animation_on_appearance_opt\");\n        return [\n            { className: \"o_anim_fade_in\", label: \"Fade\" },\n            { className: \"o_anim_slide_in\", label: \"Slide\", directionClass: \"o_anim_from_right\" },\n            { className: \"o_anim_bounce_in\", label: \"Bounce\" },\n            { className: \"o_anim_rotate_in\", label: \"Rotate\" },\n            { className: \"o_anim_zoom_out\", label: \"Zoom Out\" },\n            { className: \"o_anim_zoom_in\", label: \"Zoom In\" },\n            { className: \"o_anim_flash\", label: \"Flash\", check: isOnAppearance },\n            { className: \"o_anim_pulse\", label: \"Pulse\", check: isOnAppearance },\n            { className: \"o_anim_shake\", label: \"Shake\", check: isOnAppearance },\n            { className: \"o_anim_tada\", label: \"Tada\", check: isOnAppearance },\n            { className: \"o_anim_flip_in_x\", label: \"Flip-In-X\", check: isOnAppearance },\n            { className: \"o_anim_flip_in_y\", label: \"Flip-In-Y\", check: isOnAppearance },\n        ];\n    }\n    getDirectionsItems() {\n        const isNotSlideIn = (editingElement) =>\n            !editingElement.classList.contains(\"o_anim_slide_in\");\n        const isRotate = (editingElement) => editingElement.classList.contains(\"o_anim_rotate_in\");\n        const isNotRotate = (editingElement) => !isRotate(editingElement);\n\n        return [\n            { className: \"\", label: \"In place\", check: isNotSlideIn },\n\n            { className: \"o_anim_from_right\", label: \"From right\", check: isNotRotate },\n            { className: \"o_anim_from_left\", label: \"From left\", check: isNotRotate },\n            { className: \"o_anim_from_bottom\", label: \"From bottom\", check: isNotRotate },\n            { className: \"o_anim_from_top\", label: \"From top\", check: isNotRotate },\n\n            { className: \"o_anim_from_top_right\", label: \"From top right\", check: isRotate },\n            { className: \"o_anim_from_top_left\", label: \"From top left\", check: isRotate },\n            { className: \"o_anim_from_bottom_right\", label: \"From bottom right\", check: isRotate },\n            { className: \"o_anim_from_bottom_left\", label: \"From bottom left\", check: isRotate },\n        ];\n    }\n    async forceAnimation(editingElement) {\n        editingElement.style.animationName = \"dummy\";\n        if (editingElement.classList.contains(\"o_animate_on_scroll\")) {\n            // Trigger a DOM reflow.\n            void editingElement.offsetWidth;\n            editingElement.style.animationName = \"\";\n            this.window.dispatchEvent(new Event(\"resize\"));\n        } else {\n            // Trigger a DOM reflow (Needed to prevent the animation from\n            // being launched twice when previewing the \"Intensity\" option).\n            await new Promise((resolve) => setTimeout(resolve));\n            editingElement.classList.add(\"o_animating\");\n            this.scrollingElement.classList.add(\"o_wanim_overflow_xy_hidden\");\n            editingElement.style.animationName = \"\";\n            editingElement.addEventListener(\n                \"animationend\",\n                () => {\n                    this.scrollingElement.classList.remove(\"o_wanim_overflow_xy_hidden\");\n                    editingElement.classList.remove(\"o_animating\");\n                },\n                { once: true }\n            );\n        }\n    }\n\n    /**\n     *\n     * @returns {{element: HTMLElement, onReset: Function}|{}}\n     */\n    getAnimatedTextOrCreateDefault() {\n        const resetAnimatedText = (el) => {\n            const cursors = this.dependencies.selection.preserveSelection();\n            el.replaceWith(...el.childNodes);\n            cursors.restore();\n            this.dependencies.history.addStep();\n        };\n\n        const existingAnimatedTextEl = this.getAnimatedText();\n        if (existingAnimatedTextEl) {\n            return { element: existingAnimatedTextEl, onReset: resetAnimatedText };\n        }\n        const savePoint = this.dependencies.history.makeSavePoint();\n        const { element: createdAnimatedTextEl, didRemoveOtherTextAnimation } =\n            this.createDefaultTextAnimation();\n        if (createdAnimatedTextEl) {\n            return {\n                element: createdAnimatedTextEl,\n                onReset: didRemoveOtherTextAnimation ? resetAnimatedText : savePoint,\n            };\n        }\n        savePoint();\n        this.services.notification.add(\n            _t(\n                \"Cannot apply this option on current text selection. Try clearing the format and try again.\"\n            ),\n            { type: \"danger\", sticky: true }\n        );\n        return {};\n    }\n    /**\n     * @return {HTMLElement?} The `commonAncestorContainer` after the split\n     * (null if splits are prevented by an unsplittable node)\n     */\n    splitForAnimatedText({ anchorNode, focusNode, commonAncestorContainer }) {\n        let commonAncestor = commonAncestorContainer;\n        for (let [node, forward] of [\n            [anchorNode, true],\n            [focusNode, false],\n        ]) {\n            let needToMeetCommonAncestor =\n                node !== commonAncestor && node.parentNode !== commonAncestor;\n            let needToMeetAnimatedTextAncestor = !!closestElement(node, \".o_animated_text\");\n            let updatedCommonAncestor = needToMeetCommonAncestor ? undefined : commonAncestor;\n\n            // Go up to the common ancestor of the selection, or to the\n            // containing animated text (whichever is the furthest)\n            while (needToMeetCommonAncestor || needToMeetAnimatedTextAncestor) {\n                if (\n                    needToMeetAnimatedTextAncestor &&\n                    node.parentNode.classList.contains(\"o_animated_text\")\n                ) {\n                    needToMeetAnimatedTextAncestor = false;\n                }\n                const updatingCommonAncestor = commonAncestor === node.parentNode;\n                const splitIndex = childNodeIndex(node);\n                if (forward ? splitIndex > 0 : splitIndex < node.parentNode.childNodes.length - 1) {\n                    // Split the node if needed, abort if unsplittable (unless it is animated text)\n                    if (\n                        this.dependencies.split.isUnsplittable(node.parentNode) &&\n                        !node.parentNode.classList.contains(\"o_animated_text\")\n                    ) {\n                        return;\n                    }\n                    node = this.dependencies.split.splitElement(\n                        node.parentNode,\n                        splitIndex + (forward ? 0 : 1)\n                    )[forward ? 1 : 0];\n                } else {\n                    node = node.parentNode;\n                }\n                if (updatingCommonAncestor) {\n                    updatedCommonAncestor = node.parentNode;\n                }\n                if (needToMeetCommonAncestor && node.parentNode === commonAncestor) {\n                    needToMeetCommonAncestor = false;\n                }\n            }\n            commonAncestor = updatedCommonAncestor || commonAncestor;\n        }\n        return commonAncestor;\n    }\n    /**\n     * Create a span with the default animation, on the selection\n     *\n     * @returns {{element: HTMLElement, didRemoveOtherTextAnimation: boolean}|{}}\n     */\n    createDefaultTextAnimation() {\n        /*\n        We need to create 1 element with the content of the selection to set the\n        text animation. This element must be the only animated text element for\n        the selected text\n\n        To be able to create 1 new element containing the selection, we need to\n        split the elements that are descendants of the common ancestor and that\n        contains one end of the selection.\n\n        To remove any other overlapping animation on text, we need to:\n        - remove the animation on the part of a splitted element that falls\n          inside the selection\n        - split ancestor animated text that fully contains the selection, to\n          remove the animation on the part containing the selection\n        - remove text animation inside of the created element\n\n        If these splits would split an unsplittable node, we abort\n        */\n        const selection = this.dependencies.split.splitSelection();\n        const commonAncestor = this.splitForAnimatedText(selection);\n        if (!commonAncestor) {\n            return {};\n        }\n        const { startContainer, endContainer, direction } = selection;\n\n        const range = new Range();\n        range.setStartBefore(\n            findFurthest(startContainer, commonAncestor, () => true) || startContainer\n        );\n        range.setEndAfter(findFurthest(endContainer, commonAncestor, () => true) || endContainer);\n        const span = this.document.createElement(\"span\");\n        range.surroundContents(span);\n        // Remove animated text inside the span and containing the span (the ancestors have been split so it only contains the span)\n        let didRemoveOtherTextAnimation = false;\n        for (const node of [\n            ...span.querySelectorAll(\".o_animated_text\"),\n            ...ancestors(span, this.editable).filter((n) =>\n                n.classList.contains(\"o_animated_text\")\n            ),\n        ]) {\n            node.replaceWith(...node.childNodes);\n            didRemoveOtherTextAnimation = true;\n        }\n        span.classList.add(\"o_animated_text\", \"o_animate_preview\");\n        span.classList.add(\"o_animate\", \"o_anim_fade_in\"); // default animation\n        this.dependencies.selection.setSelection(\n            direction === DIRECTIONS.RIGHT\n                ? {\n                      anchorNode: span,\n                      anchorOffset: 0,\n                      focusNode: span,\n                      focusOffset: nodeSize(span),\n                  }\n                : {\n                      anchorNode: span,\n                      anchorOffset: nodeSize(span),\n                      focusNode: span,\n                      focusOffset: 0,\n                  }\n        );\n        this.dependencies.history.addStep();\n\n        return { element: span, didRemoveOtherTextAnimation };\n    }\n    /**\n     * Returns the element that is an animated text that corresponds to the\n     * current selection (if there is any)\n     *\n     * @returns {HTMLElement?}\n     */\n    getAnimatedText() {\n        const selection = this.dependencies.selection.getSelectionData().editableSelection;\n        const ancestor = closestElement(selection.commonAncestorContainer, \".o_animated_text\");\n        if (ancestor) {\n            const selectionText = selection.textContent().replace(/\\s+/g, \" \").trim();\n            const ancestorText = ancestor.innerText.replace(/\\s+/g, \" \").trim();\n            if (selection.isCollapsed || selectionText === ancestorText) {\n                return ancestor;\n            }\n        }\n    }\n    isAnimatedTextActive() {\n        return !!this.getAnimatedText();\n    }\n    isAnimatedTextDisabled() {\n        return 2 <= this.dependencies.selection.getTargetedNodes().size;\n    }\n\n    normalize(root) {\n        const previewEls = [...root.querySelectorAll(\".o_animate_preview\")];\n        if (root.classList.contains(\"o_animate_preview\")) {\n            previewEls.push(root);\n        }\n        for (const el of previewEls) {\n            if (el.classList.contains(\"o_animate\")) {\n                el.classList.remove(\"o_animate_preview\");\n            }\n        }\n\n        const animateEls = [...root.querySelectorAll(\".o_animate\")];\n        if (root.classList.contains(\"o_animate\")) {\n            animateEls.push(root);\n        }\n        for (const el of animateEls) {\n            if (!el.classList.contains(\"o_animate_preview\")) {\n                el.classList.add(\"o_animate_preview\");\n            }\n        }\n        const animateImg = animateEls\n            .map((el) => (el.tagName === \"IMG\" && el) || el.querySelectorAll(\"img\"))\n            .flat()\n            .filter(Boolean);\n        for (const img of animateImg) {\n            img.loading = \"eager\";\n        }\n    }\n    cleanForSave({ root }) {\n        for (const el of root.querySelectorAll(\".o_animate_preview\")) {\n            el.classList.remove(\"o_animate_preview\");\n        }\n    }\n}\n\nexport class SetAnimationModeAction extends BuilderAction {\n    static id = \"setAnimationMode\";\n    static dependencies = [\"animateOption\"];\n    setup() {\n        this.animationWithFadein = [\"onAppearance\", \"onScroll\"];\n        this.scrollingElement = getScrollingElement(this.document);\n    }\n    // todo: to remove after having the commit of louis\n    isApplied() {\n        return true;\n    }\n    async clean({ editingElement, value: effectName, nextAction }) {\n        this.scrollingElement.classList.remove(\"o_wanim_overflow_xy_hidden\");\n        editingElement.classList.remove(\n            \"o_animating\",\n            \"o_animate_both_scroll\",\n            \"o_visible\",\n            \"o_animated\",\n            \"o_animate_out\"\n        );\n        editingElement.style.animationDelay = \"\";\n        editingElement.style.animationPlayState = \"\";\n        editingElement.style.animationName = \"\";\n        editingElement.style.visibility = \"\";\n\n        if (effectName === \"onScroll\") {\n            delete editingElement.dataset.scrollZoneStart;\n            delete editingElement.dataset.scrollZoneEnd;\n        }\n        if (effectName === \"onHover\") {\n            // Use getResource instead of this.dependencies as imageHover is not\n            // included in translation. This implementation is a hack and could\n            // be improved.\n            await this.getResource(\"remove_hover_effect_handlers\")[0](editingElement);\n        }\n\n        const isNextAnimationFadein = this.animationWithFadein.includes(nextAction.value);\n        if (!isNextAnimationFadein) {\n            this._removeEffectAndDirectionClasses(editingElement.classList);\n            editingElement.style.setProperty(\"--wanim-intensity\", \"\");\n            editingElement.style.animationDuration = \"\";\n            this._setImagesLazyLoading(editingElement);\n        }\n    }\n\n    async apply({ editingElement, value: effectName, params: { forceAnimation } }) {\n        if (this.animationWithFadein.includes(effectName)) {\n            editingElement.classList.add(\"o_anim_fade_in\");\n        }\n        if (effectName === \"onScroll\") {\n            editingElement.dataset.scrollZoneStart = 0;\n            editingElement.dataset.scrollZoneEnd = 100;\n        }\n        if (effectName === \"onHover\") {\n            // Use getResource instead of this.dependencies as imageHover is not\n            // included in translation. This implementation is a hack and could\n            // be improved.\n            await this.getResource(\"set_hover_effect_handlers\")[0](editingElement);\n        }\n        if (forceAnimation) {\n            this.dependencies.animateOption.forceAnimation(editingElement);\n        }\n    }\n    /**\n     * Adds the lazy loading on images because animated images can appear before\n     * or after their parents and cause bugs in the animations. To put \"lazy\"\n     * back on the \"loading\" attribute, we simply remove the attribute as it is\n     * automatically added on page load.\n     *\n     * @private\n     */\n    _setImagesLazyLoading(editingElement) {\n        const imgEls = editingElement.matches(\"img\")\n            ? [editingElement]\n            : editingElement.querySelectorAll(\"img\");\n        for (const imgEl of imgEls) {\n            // Let the automatic system add the loading attribute\n            imgEl.removeAttribute(\"loading\");\n        }\n    }\n    _removeEffectAndDirectionClasses(targetClassList) {\n        const classes = this.dependencies.animateOption\n            .getEffectsItems()\n            .map(({ className }) => className)\n            .concat(\n                this.dependencies.animateOption\n                    .getDirectionsItems()\n                    .map(({ className }) => className)\n                    .filter(Boolean)\n            );\n\n        const classesToRemove = intersect(classes, [...targetClassList]);\n        for (const className of classesToRemove) {\n            targetClassList.remove(className);\n        }\n    }\n}\nexport class SetAnimateIntensityAction extends BuilderAction {\n    static id = \"setAnimateIntensity\";\n    static dependencies = [\"animateOption\"];\n    getValue({ editingElement }) {\n        const intensity = parseInt(\n            this.window.getComputedStyle(editingElement).getPropertyValue(\"--wanim-intensity\")\n        );\n        return intensity;\n    }\n    apply({ editingElement, value }) {\n        editingElement.style.setProperty(\"--wanim-intensity\", `${value}`);\n        this.dependencies.animateOption.forceAnimation(editingElement);\n    }\n}\nexport class ForceAnimationAction extends BuilderAction {\n    static id = \"forceAnimation\";\n    static dependencies = [\"animateOption\"];\n    // todo: to remove after having the commit of louis\n    isActive() {\n        return true;\n    }\n    apply({ editingElement }) {\n        this.dependencies.animateOption.forceAnimation(editingElement);\n    }\n}\nexport class SetAnimationEffectAction extends BuilderAction {\n    static id = \"setAnimationEffect\";\n    static dependencies = [\"animateOption\"];\n    isApplied({ editingElement, value: className }) {\n        return editingElement.classList.contains(className);\n    }\n    clean({ editingElement }) {\n        const classNames = this.dependencies.animateOption\n            .getEffectsItems()\n            .map(({ className }) => className)\n            .concat(\n                this.dependencies.animateOption\n                    .getDirectionsItems()\n                    .map(({ className }) => className)\n            );\n        for (const className of classNames) {\n            if (editingElement.classList.contains(className)) {\n                editingElement.classList.remove(className);\n            }\n        }\n    }\n    apply({ editingElement, params: { mainParam: directionClassName }, value: effectClassName }) {\n        if (directionClassName) {\n            editingElement.classList.add(directionClassName);\n        }\n        editingElement.classList.add(effectClassName);\n        this.dependencies.animateOption.forceAnimation(editingElement);\n    }\n}\n\nregistry.category(\"website-plugins\").add(AnimateOptionPlugin.id, AnimateOptionPlugin);\n\nfunction intersect(a, b) {\n    return a.filter((value) => b.includes(value));\n}\n", "import { Component, onMounted, onWillDestroy, useChildSubEnv, useRef, useState } from \"@odoo/owl\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\nimport { AnimateOption } from \"./animate_option\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { DependencyManager } from \"@html_builder/core/dependency_manager\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { POSITION_BUS } from \"@web/core/position/position_hook\";\n\nclass AnimateTextPopover extends BaseOptionComponent {\n    static template = \"website_builder.AnimateTextPopover\";\n    static props = {\n        animateOptionProps: AnimateOption.props,\n        onReset: Function,\n\n        // Popover service\n        close: { type: Function, optional: true },\n    };\n    static components = { AnimateOption };\n\n    setup() {\n        super.setup();\n        this.contentRef = useRef(\"content\");\n        this.resizeObserver = new ResizeObserver(() => {\n            this.env[POSITION_BUS]?.trigger(\"update\");\n        });\n        onMounted(() => {\n            this.resizeObserver.observe(this.contentRef.el);\n        });\n        onWillDestroy(() => {\n            this.resizeObserver.disconnect();\n        });\n    }\n}\n\nexport class AnimateText extends Component {\n    static template = \"website_builder.AnimateText\";\n    static props = {\n        ...toolbarButtonProps,\n        config: { type: Object, shape: { editor: Object, editorBus: Object } },\n        animateOptionProps: AnimateOption.props,\n        getAnimatedTextOrCreateDefault: Function,\n        isActive: Function,\n        isDisabled: Function,\n    };\n\n    setup() {\n        this.state = useState({});\n        this.updateState();\n\n        this.root = useRef(\"root\");\n        useChildSubEnv({\n            dependencyManager: new DependencyManager(),\n            getEditingElement: () => this.activeElement,\n            getEditingElements: () => (this.activeElement ? [this.activeElement] : []),\n            weContext: {},\n            editor: this.props.config.editor,\n            editorBus: this.props.config.editorBus,\n            services: this.props.config.editor.services,\n        });\n        this.popover = usePopover(AnimateTextPopover, {\n            env: this.__owl__.childEnv,\n            onClose: () => {\n                if (!this.props.config.editor.isDestroyed) {\n                    this.updateState();\n                }\n            },\n        });\n    }\n\n    onClick() {\n        if (this.popover.isOpen) {\n            return;\n        }\n        const { element, onReset } = this.props.getAnimatedTextOrCreateDefault();\n        if (!element) {\n            return;\n        }\n        this.activeElement = element;\n\n        this.updateState();\n        this.popover.open(this.root.el, {\n            animateOptionProps: this.props.animateOptionProps,\n            onReset: () => {\n                onReset(this.activeElement);\n                this.popover.close();\n            },\n        });\n    }\n\n    updateState() {\n        this.state.isActive = this.props.isActive();\n        this.state.isDisabled = this.props.isDisabled();\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { after } from \"@html_builder/utils/option_sequence\";\nimport { WEBSITE_BACKGROUND_OPTIONS } from \"@website/builder/option_sequence\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nclass SetItemTextAction extends BuilderAction {\n    static id = \"setItemTextAction\";\n    static dependencies = [\"edit_interaction\"];\n\n    getValue({ editingElement, params }) {\n        return editingElement.textContent;\n    }\n    apply({ editingElement, value, params }) {\n        editingElement.textContent = value;\n    }\n}\n\nexport class AnnouncementScrollOption extends BaseOptionComponent {\n    static template = \"website.AnnouncementScrollOption\";\n    static selector = \"section.s_announcement_scroll\";\n}\n\nexport class AnnouncementScrollOptionPlugin extends Plugin {\n    static id = \"announcementScrollOptionPlugin\";\n    selector = AnnouncementScrollOption.selector;\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(after(WEBSITE_BACKGROUND_OPTIONS), AnnouncementScrollOption),\n        ],\n        builder_actions: {\n            SetItemTextAction,\n        },\n    };\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(AnnouncementScrollOptionPlugin.id, AnnouncementScrollOptionPlugin);\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { BackgroundOption } from \"@html_builder/plugins/background_option/background_option\";\nimport { ParallaxOption } from \"./parallax_option\";\nimport { useBackgroundOption } from \"@html_builder/plugins/background_option/background_hook\";\n\nexport class BaseWebsiteBackgroundOption extends BaseOptionComponent {\n    static template = \"website.WebsiteBackgroundOption\";\n    static components = {\n        ...BackgroundOption.components,\n        ParallaxOption,\n    };\n    static props = {\n        ...BackgroundOption.props,\n        withColors: { type: Boolean, optional: true },\n        withImages: { type: Boolean, optional: true },\n        withColorCombinations: { type: Boolean, optional: true },\n        withVideos: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        ...BackgroundOption.defaultProps,\n        withColors: true,\n        withImages: true,\n        withColorCombinations: true,\n        withVideos: false,\n    };\n    setup() {\n        super.setup();\n        const { showColorFilter } = useBackgroundOption(this.isActiveItem);\n        this.showColorFilter = () => showColorFilter() || this.isActiveItem(\"toggle_bg_video_id\");\n        this.websiteBgOptionDomState = useDomState((el) => ({\n            // Only search for .s_parallax_bg that are direct children\n            applyTo: el.querySelector(\":scope > .s_parallax_bg\") ? \".s_parallax_bg\" : \"\",\n        }));\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { VideoSelector } from \"@html_editor/main/media/media_dialog/video_selector\";\n\n/**\n * @typedef { Object } WebsiteBackgroundVideoShared\n * @property { WebsiteBackgroundVideoPlugin['loadReplaceBackgroundVideo'] } loadReplaceBackgroundVideo\n * @property { WebsiteBackgroundVideoPlugin['applyReplaceBackgroundVideo'] } applyReplaceBackgroundVideo\n */\n\nfunction getBgVideoOrParallax(editingElement) {\n    // Make sure parallax and video element are considered to be below the\n    // color filters / shape\n    const bgVideoEl = editingElement.querySelector(\":scope > .o_bg_video_container\");\n    if (bgVideoEl) {\n        return bgVideoEl;\n    }\n    return editingElement.querySelector(\":scope > .s_parallax_bg\");\n}\n\nclass WebsiteBackgroundImageOptionPlugin extends Plugin {\n    static id = \"websiteBackgroundImageOptionPlugin\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        background_filter_target_providers: withSequence(10, getBgVideoOrParallax),\n    };\n}\n\nclass WebsiteBackgroundShapeOptionPlugin extends Plugin {\n    static id = \"websiteBackgroundShapeOptionPlugin\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        background_shape_target_providers: withSequence(10, getBgVideoOrParallax),\n    };\n}\n\nclass WebsiteBackgroundVideoPlugin extends Plugin {\n    static id = \"websiteBackgroundVideoPlugin\";\n    static dependencies = [\"media\"];\n    static shared = [\n        \"loadReplaceBackgroundVideo\",\n        \"applyReplaceBackgroundVideo\",\n        \"removeBackgroundVideo\",\n    ];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_actions: {\n            ToggleBgVideoAction,\n            RemoveBgVideoAction,\n            ReplaceBgVideoAction,\n        },\n        system_node_selectors: \".o_bg_video_container\",\n    };\n    loadReplaceBackgroundVideo() {\n        return new Promise((resolve) => {\n            const onClose = this.dependencies.media.openMediaDialog({\n                extraTabs: [\n                    {\n                        id: \"VIDEO_BACKGROUND\",\n                        title: _t(\"Videos\"),\n                        Component: VideoSelector,\n                        props: {\n                            isForBgVideo: true,\n                            vimeoPreviewIds: [\n                                \"528686125\",\n                                \"430330731\",\n                                \"509869821\",\n                                \"397142251\",\n                                \"763851966\",\n                                \"486931161\",\n                                \"499761556\",\n                                \"1092009120\",\n                                \"728584384\",\n                                \"865314310\",\n                                \"511727912\",\n                                \"466830211\",\n                            ],\n                        },\n                    },\n                ],\n                visibleTabs: [\"VIDEO_BACKGROUND\"],\n                save: (media) => {\n                    resolve(media.querySelector(\"iframe\").src);\n                },\n            });\n            onClose.then(resolve);\n        });\n    }\n    applyReplaceBackgroundVideo({\n        editingElement,\n        loadResult: mediaSrc,\n        params: { forceClean = false },\n    }) {\n        if (!forceClean && !mediaSrc) {\n            // No video has been chosen by the user on the media dialog\n            return;\n        }\n        editingElement.classList.toggle(\"o_background_video\", !!mediaSrc);\n        if (mediaSrc) {\n            editingElement.dataset.bgVideoSrc = mediaSrc;\n        } else {\n            delete editingElement.dataset.bgVideoSrc;\n        }\n    }\n    /**\n     * Remove the current background video and notify listeners.\n     *\n     * @param {Object} context\n     * @param {HTMLElement} context.editingElement\n     * @param {Object} [context.params]\n     */\n    removeBackgroundVideo({ editingElement, params }) {\n        editingElement.querySelector(\":scope > .o_we_bg_filter\")?.remove();\n        this.applyReplaceBackgroundVideo({\n            editingElement,\n            loadResult: \"\",\n            params: { ...params, forceClean: true },\n        });\n        this.dispatchTo(\"on_bg_image_hide_handlers\", editingElement);\n    }\n}\n\nexport class ToggleBgVideoAction extends BuilderAction {\n    static id = \"toggleBgVideo\";\n    static dependencies = [\"websiteBackgroundVideoPlugin\"];\n    load(context) {\n        return this.dependencies.websiteBackgroundVideoPlugin.loadReplaceBackgroundVideo(context);\n    }\n    apply({ editingElement, params, loadResult }) {\n        this.dependencies.websiteBackgroundVideoPlugin.applyReplaceBackgroundVideo({\n            editingElement: editingElement,\n            params: params,\n            loadResult: loadResult,\n        });\n        this.dispatchTo(\"on_bg_image_hide_handlers\", editingElement);\n    }\n    isApplied({ editingElement }) {\n        return editingElement.classList.contains(\"o_background_video\");\n    }\n    clean(context) {\n        this.dependencies.websiteBackgroundVideoPlugin.removeBackgroundVideo(context);\n    }\n}\n\nexport class RemoveBgVideoAction extends BuilderAction {\n    static id = \"removeBgVideo\";\n    static dependencies = [\"websiteBackgroundVideoPlugin\"];\n    apply(context) {\n        this.dependencies.websiteBackgroundVideoPlugin.removeBackgroundVideo(context);\n    }\n}\n\nexport class ReplaceBgVideoAction extends BuilderAction {\n    static id = \"replaceBgVideo\";\n    static dependencies = [\"websiteBackgroundVideoPlugin\"];\n    load(context) {\n        return this.dependencies.websiteBackgroundVideoPlugin.loadReplaceBackgroundVideo(context);\n    }\n    apply(context) {\n        return this.dependencies.websiteBackgroundVideoPlugin.applyReplaceBackgroundVideo(context);\n    }\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(WebsiteBackgroundVideoPlugin.id, WebsiteBackgroundVideoPlugin);\n\nregistry\n    .category(\"website-plugins\")\n    .add(WebsiteBackgroundImageOptionPlugin.id, WebsiteBackgroundImageOptionPlugin);\n\nregistry\n    .category(\"website-plugins\")\n    .add(WebsiteBackgroundShapeOptionPlugin.id, WebsiteBackgroundShapeOptionPlugin);\n", "import { after } from \"@html_builder/utils/option_sequence\";\nimport { WEBSITE_BACKGROUND_OPTIONS } from \"@website/builder/option_sequence\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { BorderConfigurator } from \"@html_builder/plugins/border_configurator_option\";\nimport { ShadowOption } from \"@html_builder/plugins/shadow_option\";\n\nexport class BentoBorderOption extends BaseOptionComponent {\n    static template = \"html_builder.BentoBorderOption\";\n    static selector = \".s_bento_banner section[data-name='Card'], .s_bento_block_card\";\n    static components = { BorderConfigurator, ShadowOption };\n}\n\nclass BentoBorderOptionPlugin extends Plugin {\n    static id = \"BentoBorderOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(after(WEBSITE_BACKGROUND_OPTIONS), BentoBorderOption)],\n    };\n}\nregistry.category(\"website-plugins\").add(BentoBorderOptionPlugin.id, BentoBorderOptionPlugin);\n", "import { after, ANIMATE, END } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { BaseWebsiteBackgroundOption } from \"@website/builder/plugins/options/background_option\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { BorderConfigurator } from \"@html_builder/plugins/border_configurator_option\";\nimport { ShadowOption } from \"@html_builder/plugins/shadow_option\";\n\nexport class BlockquoteOption extends BaseOptionComponent {\n    static template = \"website.BlockquoteOption\";\n    static selector = \".s_blockquote\";\n    static components = { BorderConfigurator, ShadowOption };\n}\n\nexport class WebsiteBackgroundBlockquoteOption extends BaseWebsiteBackgroundOption {\n    static selector = \".s_blockquote\";\n    static defaultProps = {\n        withColors: true,\n        withImages: true,\n        withShapes: true,\n        withColorCombinations: true,\n    };\n}\n\nclass BlockquoteOptionPlugin extends Plugin {\n    static id = \"blockquoteOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        mark_color_level_selector_params: [{ selector: \".s_blockquote\" }],\n        builder_options: [\n            withSequence(after(ANIMATE), WebsiteBackgroundBlockquoteOption),\n            withSequence(END, BlockquoteOption),\n        ],\n    };\n}\n// TODO: as in master, the position of a background image does not work\n// correctly.\nregistry.category(\"website-plugins\").add(BlockquoteOptionPlugin.id, BlockquoteOptionPlugin);\n", "import { CARD_PARENT_HANDLERS } from \"@website/builder/plugins/options/utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { BOX_BORDER_SHADOW } from \"@website/builder/option_sequence\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { BorderConfigurator } from \"@html_builder/plugins/border_configurator_option\";\nimport { ShadowOption } from \"@html_builder/plugins/shadow_option\";\n\nexport class BorderOption extends BaseOptionComponent {\n    static template = \"website.BorderOption\";\n    static selector = \"section .row > div, section:has(.s_carousel_cards)\";\n    static exclude = `.s_col_no_bgcolor, .s_col_no_bgcolor.row > div, .s_image_gallery .row > div, .s_masonry_block .s_col_no_resize, .s_text_cover .row > .o_not_editable, ${CARD_PARENT_HANDLERS}`;\n    static components = {\n        BorderConfigurator,\n        ShadowOption,\n    };\n}\n\nclass BorderOptionPlugin extends Plugin {\n    static id = \"borderOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(BOX_BORDER_SHADOW, BorderOption)],\n    };\n}\nregistry.category(\"website-plugins\").add(BorderOptionPlugin.id, BorderOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\n\nconst selector = \"a.btn\";\nconst exclude = \".s_donation_donate_btn, .s_website_form_send\";\n\nconst styleClasses = [\n    \"btn-secondary\",\n    \"btn-fill-primary\",\n    \"btn-fill-secondary\",\n    \"btn-outline-primary\",\n    \"btn-outline-secondary\",\n];\nconst sizeClasses = [\"btn-sm\", \"btn-lg\"];\n\nclass ButtonOptionPlugin extends Plugin {\n    static id = \"buttonOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        on_cloned_handlers: this.onCloned.bind(this),\n        // Drag and drop from sidebar: manage the button preview.\n        on_snippet_over_dropzone_handlers: this.onSnippetPreview.bind(this),\n        on_snippet_out_dropzone_handlers: ({ snippetEl, dragState }) =>\n            this.resetPreview(snippetEl, dragState),\n        on_snippet_dropped_over_handlers: ({ droppedEl, dragState }) =>\n            this.resetPreview(droppedEl, dragState),\n        on_snippet_dropped_handlers: this.onSnippetDropped.bind(this),\n    };\n\n    onCloned({ cloneEl }) {\n        if (cloneEl.matches(selector) && !cloneEl.matches(exclude)) {\n            this.adaptButtons(cloneEl, { adaptAppearance: false });\n        }\n    }\n\n    /**\n     * Adapts the dragged \"Button\" snippet when it is over a dropzone, in order\n     * to preview it correctly.\n     *\n     * @param {Object} - snippetEl: the dragged snippet\n     *                 - dragState: the current drag state\n     */\n    onSnippetPreview({ snippetEl, dragState }) {\n        if (snippetEl.matches(selector) && !snippetEl.matches(exclude)) {\n            const dropzoneEl = dragState.currentDropzoneEl;\n            // No need to preview if it is a grid item, as it is alone.\n            if (dropzoneEl.classList.contains(\"oe_grid_zone\")) {\n                return;\n            }\n\n            // Preview the button.\n            const initialState = this.adaptButtons(snippetEl, { isDragAndDropPreview: true });\n            // Store the restore function to undo the preview.\n            dragState.restoreButtonPreview = () => {\n                const { isWrapped, previousClassName, nextClassName } = initialState;\n                // Restore the classes and remove the added spaces.\n                snippetEl.className = initialState.buttonClassName;\n                if (previousClassName) {\n                    initialState.previousSiblingEl.className = previousClassName;\n                    snippetEl.previousSibling.remove();\n                }\n                if (nextClassName) {\n                    initialState.nextSiblingEl.className = nextClassName;\n                    snippetEl.nextSibling.remove();\n                }\n                // Unwrap the button.\n                if (isWrapped) {\n                    const wrapperEl = snippetEl.parentElement;\n                    dropzoneEl.after(snippetEl);\n                    wrapperEl.remove();\n                }\n            };\n        }\n    }\n\n    /**\n     * Resets the \"Button\" snippet drag and drop preview.\n     *\n     * @param {HTMLElement} snippetEl the snippet\n     * @param {Object} dragState the current drag state\n     */\n    resetPreview(snippetEl, dragState) {\n        if (snippetEl.matches(selector) && !snippetEl.matches(exclude)) {\n            if (\"restoreButtonPreview\" in dragState) {\n                dragState.restoreButtonPreview();\n                delete dragState.restoreButtonPreview;\n            }\n        }\n    }\n\n    /**\n     * Adapts the dropped \"Button\" snippet.\n     *\n     * @param {Object} - snippetEl: the dropped snippet.\n     */\n    onSnippetDropped({ snippetEl }) {\n        if (snippetEl.matches(selector) && !snippetEl.matches(exclude)) {\n            this.adaptButtons(snippetEl, {});\n        }\n    }\n\n    /**\n     * Checks if there are buttons before or after the target element and\n     * applies appropriate styling.\n     *\n     * @param {HTMLElement} editingElement\n     * @param {Object}\n     *   - [adaptAppearance=true]\n     *   - [isDragAndDropPreview = false]\n     * @returns {Object} the info needed to restore the drag and drop preview\n     */\n    adaptButtons(editingElement, { adaptAppearance = true, isDragAndDropPreview = false }) {\n        let previousSiblingEl = editingElement.previousElementSibling;\n        let nextSiblingEl = editingElement.nextElementSibling;\n        // If we are in the case of a drag and drop preview, ignore the\n        // dropzones.\n        if (isDragAndDropPreview) {\n            while (previousSiblingEl && previousSiblingEl.matches(\".oe_drop_zone\")) {\n                previousSiblingEl = previousSiblingEl.previousElementSibling;\n            }\n            while (nextSiblingEl && nextSiblingEl.matches(\".oe_drop_zone\")) {\n                nextSiblingEl = nextSiblingEl.nextElementSibling;\n            }\n        }\n\n        let siblingButtonEl = null;\n        const initialState = { buttonClassName: editingElement.className };\n        // When multiple buttons follow each other, they may break on 2 lines or\n        // more on mobile, so they need a margin-bottom. Also, if the button is\n        // dropped next to another button add a space between them.\n        if (nextSiblingEl?.matches(\".btn\")) {\n            initialState.nextClassName = nextSiblingEl.className;\n            nextSiblingEl.classList.add(\"mb-2\");\n            editingElement.after(\" \");\n            // It is first the next button that we put in this variable because\n            // we want to copy as a priority the style of the previous button\n            // if it exists.\n            siblingButtonEl = nextSiblingEl;\n        }\n        if (previousSiblingEl?.matches(\".btn\")) {\n            initialState.previousClassName = previousSiblingEl.className;\n            previousSiblingEl.classList.add(\"mb-2\");\n            editingElement.before(\" \");\n            siblingButtonEl = previousSiblingEl;\n        }\n        if (siblingButtonEl) {\n            editingElement.classList.add(\"mb-2\");\n        }\n        if (adaptAppearance) {\n            if (siblingButtonEl && !editingElement.matches(\".s_custom_button\")) {\n                // If the dropped button is not a custom button then we adjust\n                // its appearance to match its sibling.\n                // TODO this should consider the old option classes (for already\n                // existing buttons) + custom ?\n                const styleClass = styleClasses.find((c) => siblingButtonEl.classList.contains(c));\n                const sizeClass = sizeClasses.find((c) => siblingButtonEl.classList.contains(c));\n\n                if (styleClass) {\n                    editingElement.classList.remove(\"btn-primary\");\n                    editingElement.classList.add(styleClass);\n                }\n                if (sizeClass) {\n                    editingElement.classList.add(sizeClass);\n                }\n                if (siblingButtonEl.classList.contains(\"rounded-circle\")) {\n                    editingElement.classList.add(\"rounded-circle\");\n                }\n            } else if (!siblingButtonEl) {\n                // To align with the editor's behavior, we need to enclose the\n                // button in a <p> tag if it's not dropped within a <p> tag. We\n                // only put the dropped button in a <p> if it's not next to\n                // another button, because some snippets have buttons that are\n                // not inside a <p> (e.g. s_text_cover).\n                // TODO: this definitely needs to be fixed at web_editor level.\n                // Nothing should prevent adding buttons outside of a paragraph.\n                const btnContainerEl = editingElement.closest(\"p\");\n                if (!btnContainerEl) {\n                    const paragraphEl = document.createElement(\"p\");\n                    editingElement.parentNode.insertBefore(paragraphEl, editingElement);\n                    paragraphEl.appendChild(editingElement);\n                    initialState.isWrapped = true;\n                }\n            }\n            editingElement.classList.remove(\"s_custom_button\");\n        }\n        return { ...initialState, previousSiblingEl, nextSiblingEl };\n    }\n}\n\nregistry.category(\"website-plugins\").add(ButtonOptionPlugin.id, ButtonOptionPlugin);\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\n\nexport class CardImageAlignmentOption extends BaseOptionComponent {\n    static template = \"website.CardImageAlignmentOption\";\n    static props = {\n        label: { type: String },\n        level: { type: Number, optional: true },\n    };\n    static defaultProps = {\n        level: 0,\n    };\n\n    setup() {\n        super.setup();\n        this.state = useDomState(async (editingElement) => {\n            await this.waitForAllImageloaded(this.env.getEditingElements());\n            const coverImageWrapperEl = editingElement.querySelector(\n                \":scope > .o_card_img_wrapper\"\n            );\n            const hasCoverImage = !!coverImageWrapperEl;\n            const imageToWrapperRatio = hasCoverImage\n                ? this.getImageToWrapperRatio(coverImageWrapperEl)\n                : null;\n            const hasShape = hasCoverImage\n                ? !!coverImageWrapperEl.querySelector(\".o_card_img[data-shape]\")\n                : false;\n            // Sometimes the imageToWrapperRatio is very close to but not\n            // exactly 1. In this case, the image alignment slider would have no\n            // visible effect on the actual alignment. To avoid the slider to\n            // spawn in this case, we use a loose comparison.\n            const hasSquareRatio = Math.abs(imageToWrapperRatio - 1) < 0.001;\n            return {\n                imageToWrapperRatio,\n                show: hasCoverImage && !(hasSquareRatio || hasShape),\n            };\n        });\n    }\n\n    /**\n     * Compares the aspect ratio of the card image to its wrapper.\n     *\n     * @param {HTMLElement} editingElement\n     * @returns {number|null} Ratio comparison value:\n     *                   -  1: img and wrapper have identical aspect ratios\n     *                   - <1: img is more portrait (taller) than wrapper\n     *                   - >1: img is more landscape (wider) than wrapper\n     */\n    getImageToWrapperRatio(imageWrapperEl) {\n        const imageEl = imageWrapperEl.querySelector(\".o_card_img\");\n        const imgRatio = imageEl.naturalWidth / imageEl.naturalHeight;\n        const wrapperRatio = imageWrapperEl.offsetWidth / imageWrapperEl.offsetHeight;\n        return imgRatio / wrapperRatio;\n    }\n\n    async waitForAllImageloaded(editingElements) {\n        const promises = [];\n        for (const editingEl of editingElements) {\n            const imageEls = editingEl.matches(\"img\")\n                ? [editingEl]\n                : editingEl.querySelectorAll(\"img\");\n            for (const imageEl of imageEls) {\n                if (!imageEl.complete) {\n                    promises.push(\n                        new Promise((resolve) => {\n                            imageEl.addEventListener(\"load\", () => resolve());\n                        })\n                    );\n                }\n            }\n        }\n        await Promise.all(promises);\n    }\n}\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { CardImageAlignmentOption } from \"./card_image_alignment_option\";\n\nexport class CardImageOption extends BaseOptionComponent {\n    static template = \"website.CardImageOption\";\n    static components = { CardImageAlignmentOption };\n\n    setup() {\n        super.setup();\n        this.state = useDomState((editingElement) => ({\n            hasCoverImage: !!editingElement.querySelector(\":scope > .o_card_img_wrapper\"),\n        }));\n    }\n}\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { ClassAction } from \"@html_builder/core/core_builder_action_plugin\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\n/**\n * @typedef { Object } CardImageOptionShared\n * @property { CardImageOptionPlugin['adaptRatio'] } adaptRatio\n */\n\nconst ratiosOnlySupportedForTopImage = [\n    \"ratio ratio-4x3\",\n    \"ratio ratio-16x9\",\n    \"ratio ratio-21x9\",\n    \"ratio o_card_img_ratio_custom\",\n];\nconst imageRelatedClasses = [\n    \"o_card_img_top\",\n    \"o_card_img_horizontal\",\n    \"flex-lg-row\",\n    \"flex-lg-row-reverse\",\n];\nconst imageRelatedStyles = [\n    \"--card-img-aspect-ratio\",\n    \"--card-img-size-h\",\n    \"--card-img-ratio-align\",\n];\n\nclass CardImageOptionPlugin extends Plugin {\n    static id = \"cardImageOption\";\n    static dependencies = [\"remove\", \"history\", \"builderOptions\"];\n    static shared = [\"adaptRatio\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_actions: {\n            SetCoverImagePositionAction,\n            RemoveCoverImageAction,\n            AddCoverImageAction,\n            AlignCoverImageAction,\n        },\n    };\n\n    setup() {\n        super.setup();\n        this.classAction = new ClassAction(this);\n    }\n    /**\n     * Change unsupported ratios to the square ratio when the cover image is\n     * positioned horizontally.\n     */\n    adaptRatio(editingElement, imagePositionClass) {\n        if (imagePositionClass === \"card-img-top\") {\n            // All ratios are supported for top image\n            return;\n        }\n        const imageWrapper = editingElement.querySelector(\".o_card_img_wrapper\");\n        const asMainParam = (mainParam) => ({\n            editingElement: imageWrapper,\n            params: { mainParam },\n        });\n        for (const ratioClasses of ratiosOnlySupportedForTopImage) {\n            if (this.classAction.isApplied(asMainParam(ratioClasses))) {\n                this.classAction.clean(asMainParam(ratioClasses));\n                // Only square ratio is supported for horizontal image\n                this.classAction.apply(asMainParam(\"ratio ratio-1x1\"));\n                return;\n            }\n        }\n    }\n}\n\nexport class SetCoverImagePositionAction extends BuilderAction {\n    static id = \"setCoverImagePosition\";\n    static dependencies = [\"cardImageOption\"];\n    apply({ editingElement, params: { mainParam: className } }) {\n        const imageEl = editingElement.querySelector(\".o_card_img\");\n        imageEl.classList.add(className);\n        this.dependencies.cardImageOption.adaptRatio(editingElement, className);\n    }\n    clean({ editingElement, params: { mainParam: className } }) {\n        const imageEl = editingElement.querySelector(\".o_card_img\");\n        imageEl.classList.remove(className);\n    }\n}\nexport class RemoveCoverImageAction extends BuilderAction {\n    static id = \"removeCoverImage\";\n    static dependencies = [\"history\", \"builderOptions\", \"remove\"];\n    apply({ editingElement }) {\n        const imageWrapperEl = editingElement.querySelector(\".o_card_img_wrapper\");\n        imageWrapperEl.remove();\n        // Remove the classes and styles linked to the wrapper.\n        editingElement.classList.remove(...imageRelatedClasses);\n        imageRelatedStyles.forEach((prop) => editingElement.style.removeProperty(prop));\n    }\n}\nexport class AddCoverImageAction extends BuilderAction {\n    static id = \"addCoverImage\";\n    apply({ editingElement }) {\n        const imageWrapper = renderToElement(\"website.s_card.imageWrapper\");\n        editingElement.prepend(imageWrapper);\n        editingElement.classList.add(\"o_card_img_top\");\n    }\n}\nexport class AlignCoverImageAction extends BuilderAction {\n    static id = \"alignCoverImage\";\n    apply({ editingElement, params: { mainParam: direction } }) {\n        const imgWrapper = editingElement.querySelector(\".o_card_img_wrapper\");\n        imgWrapper.classList.toggle(\"o_card_img_adjust_v\", direction === \"vertical\");\n        imgWrapper.classList.toggle(\"o_card_img_adjust_h\", direction === \"horizontal\");\n    }\n}\n\nregistry.category(\"website-plugins\").add(CardImageOptionPlugin.id, CardImageOptionPlugin);\n", "import { BaseOptionComponent, useGetItemValue } from \"@html_builder/core/utils\";\nimport { BaseWebsiteBackgroundOption } from \"@website/builder/plugins/options/background_option\";\nimport { CardImageOption } from \"./card_image_option\";\nimport { BorderConfigurator } from \"@html_builder/plugins/border_configurator_option\";\nimport { ShadowOption } from \"@html_builder/plugins/shadow_option\";\nimport { CARD_DISABLE_WIDTH_APPLY_TO, CARD_PARENT_HANDLERS } from \"./utils\";\n\nexport class BaseCardOption extends BaseOptionComponent {\n    static template = \"website.CardOption\";\n    static components = {\n        CardImageOption,\n        WebsiteBackgroundOption: BaseWebsiteBackgroundOption,\n        BorderConfigurator,\n        ShadowOption,\n    };\n    static props = {\n        disableWidth: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        disableWidth: false,\n    };\n    setup() {\n        super.setup();\n        this.getItemValue = useGetItemValue();\n    }\n}\n\nexport class CardOption extends BaseCardOption {\n    static selector = \".s_card\";\n    static exclude = `div:is(${CARD_PARENT_HANDLERS}) > .s_card`;\n}\n\nexport class CardWithoutWidthOption extends BaseCardOption {\n    static selector = CARD_PARENT_HANDLERS;\n    static applyTo = CARD_DISABLE_WIDTH_APPLY_TO;\n    static defaultProps = {\n        disableWidth: true,\n    };\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport {\n    CARD_DISABLE_WIDTH_APPLY_TO,\n    CARD_PARENT_HANDLERS,\n    WEBSITE_BG_APPLY_TO,\n} from \"@website/builder/plugins/options/utils\";\nimport { BaseWebsiteBackgroundOption } from \"./background_option\";\nimport { CarouselCardsItemOption } from \"./carousel_cards_item_option\";\nimport { CardOption, CardWithoutWidthOption } from \"./card_option\";\n\nexport class WebsiteBackgroundCardOption extends BaseWebsiteBackgroundOption {\n    static selector = CARD_PARENT_HANDLERS;\n    static applyTo = WEBSITE_BG_APPLY_TO;\n    static defaultProps = {\n        withColors: true,\n        withImages: true,\n        withShapes: true,\n        withColorCombinations: true,\n    };\n}\n\nclass CardOptionPlugin extends Plugin {\n    static id = \"cardOption\";\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            CardOption,\n            CardWithoutWidthOption,\n            WebsiteBackgroundCardOption,\n            CarouselCardsItemOption,\n        ],\n        mark_color_level_selector_params: [\n            { selector: CardOption.selector, exclude: CardOption.exclude },\n            { selector: CARD_PARENT_HANDLERS, applyTo: CARD_DISABLE_WIDTH_APPLY_TO },\n            { selector: CARD_PARENT_HANDLERS, applyTo: WEBSITE_BG_APPLY_TO },\n        ],\n    };\n}\n\nregistry.category(\"website-plugins\").add(CardOptionPlugin.id, CardOptionPlugin);\n", "import { ClassAction, StyleAction } from \"@html_builder/core/core_builder_action_plugin\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\n\nclass CardWidthOptionPlugin extends Plugin {\n    static id = \"cardWidthOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_actions: {\n            SetCardWidthAction,\n            SetCardAlignmentAction,\n        },\n    };\n}\n\nregistry.category(\"website-plugins\").add(CardWidthOptionPlugin.id, CardWidthOptionPlugin);\n\nexport class SetCardAlignmentAction extends ClassAction {\n    static id = \"setCardAlignment\";\n    isApplied({ editingElement: el, params: { mainParam: classNames } }) {\n        if (classNames === \"me-auto\") {\n            return ![\"mx-auto\", \"ms-auto\"].some((cls) => el.classList.contains(cls));\n        }\n        return super.isApplied(...arguments);\n    }\n}\n\nexport class SetCardWidthAction extends StyleAction {\n    static id = \"setCardWidth\";\n    getValue(...args) {\n        const value = super.getValue(...args);\n        return value.includes(\"%\") ? value : \"100%\";\n    }\n}\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { CardImageAlignmentOption } from \"./card_image_alignment_option\";\n\nexport class CarouselCardsItemOption extends BaseOptionComponent {\n    static template = \"website.CarouselCardsItemOption\";\n    static selector = \".s_carousel_cards_item\";\n    static applyTo = \":scope > .s_carousel_cards_card\";\n    static components = {\n        CardImageAlignmentOption,\n    };\n}\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { SNIPPET_SPECIFIC_END } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\n\nexport class CarouselSlidesOption extends BaseOptionComponent {\n    static template = \"website.CarouselSlidesOption\";\n    static selector = \".carousel .carousel-item\";\n    static exclude = \".s_image_gallery .carousel-item\";\n}\n\nexport class CarouselSlidesOptionPlugin extends Plugin {\n    static id = \"carouselSlidesOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(SNIPPET_SPECIFIC_END, CarouselSlidesOption)],\n        builder_actions: {\n            MakeSlideClickableAction,\n            SetSlideAnchorUrlAction,\n        },\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        legit_empty_link_predicates: (linkEl) => linkEl.matches(\".carousel-item a.slide-link\"),\n    };\n\n    /**\n     * Remove `clickable-slide` class from slides when there is no link element.\n     * TODO: Find a better approach. The class is currently used so the \"active\"\n     * state of the `BuilderCheckbox` can be taken into account.\n     * It would probably be better to handle this via an option state, or adapt\n     * the`BuilderCheckbox to expose its 'checkbox active state' when no action\n     * is linked to it...\n     *\n     * @param {HTMLElement} root\n     */\n    cleanForSave({ root }) {\n        const noLinkSlideEls = root.querySelectorAll(\n            \".carousel-item.clickable-slide:not(:has(.slide-link))\"\n        );\n        for (const slideEl of noLinkSlideEls) {\n            slideEl.classList.remove(\"clickable-slide\");\n        }\n    }\n}\n\nclass MakeSlideClickableAction extends BuilderAction {\n    static id = \"makeSlideClickable\";\n    setup() {\n        this.preview = false;\n    }\n    clean({ editingElement }) {\n        // Remove unnecessary link from the slide when toggled off.\n        const linkEl = editingElement.querySelector(\"a.slide-link\");\n        linkEl?.remove();\n    }\n}\n\n/**\n * Custom action to add, update, or remove a slide-link for clickable carousel\n * slides.\n */\nclass SetSlideAnchorUrlAction extends BuilderAction {\n    static id = \"setSlideAnchorUrl\";\n    setup() {\n        this.preview = false;\n    }\n    apply({ editingElement, value }) {\n        const url = value;\n        const linkEl = editingElement.querySelector(\"a.slide-link\");\n\n        if (!url) {\n            linkEl.remove();\n            return;\n        }\n        if (linkEl) {\n            linkEl.setAttribute(\"href\", url);\n            return;\n        }\n        const anchorEl = document.createElement(\"a\");\n        anchorEl.className = \"slide-link position-absolute top-0 start-0 w-100 h-100 d-none\";\n        anchorEl.setAttribute(\"href\", url);\n        anchorEl.style.zIndex = 100;\n        editingElement.prepend(anchorEl);\n    }\n    getValue({ editingElement }) {\n        const linkEl = editingElement.querySelector(\"a.slide-link\");\n        return linkEl?.getAttribute(\"href\") || \"\";\n    }\n}\n\nregistry.category(\"website-plugins\").add(CarouselSlidesOptionPlugin.id, CarouselSlidesOptionPlugin);\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { getCSSVariableValue } from \"@html_editor/utils/formatting\";\nimport { useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { isCSSColor } from \"@web/core/utils/colors\";\n\nexport const DATASET_KEY_PREFIX = \"chart_dataset_\";\n\nexport function getColor(color, win, doc) {\n    if (!color) {\n        return \"\";\n    }\n    return isCSSColor(color)\n        ? color\n        : getCSSVariableValue(color, win.getComputedStyle(doc.documentElement));\n}\n\nexport class ChartOption extends BaseOptionComponent {\n    static template = \"website.ChartOption\";\n    static selector = \".s_chart\";\n    static dependencies = [\"chartOptionPlugin\"];\n\n    setup() {\n        super.setup();\n\n        // Here for compatibility with previous versions (< 18.3).\n        this.env.getEditingElement().dataset.data = JSON.stringify(\n            this.prepareData(this.env.getEditingElement())\n        );\n\n        this.state = useState({ currentCell: {} });\n\n        this.domState = useDomState((editingElement) => ({\n            data: this.getData(editingElement),\n            isPieChart: this.isPieChart(editingElement),\n        }));\n        this.setDefaultState();\n    }\n\n    /**\n     * Resets the current cell to the topleft cell\n     * and sets the colorpicker labels based on chart type.\n     */\n    setDefaultState() {\n        const { backgroundLabel, borderLabel } = this.getColorpickersLabels(\n            this.domState.isPieChart\n        );\n        this.updateCurrentCell({\n            backgroundLabel,\n            borderLabel,\n            datasetIndex: 0,\n            dataIndex: 0,\n        });\n    }\n\n    getData(editingElement) {\n        return JSON.parse(editingElement.dataset.data);\n    }\n    /**\n     * Parse the data from the DOM and make sure there are `key` properties\n     * where needed by the component API.\n     *\n     * @param {HTMLElement} editingElement\n     * @returns {Object}\n     */\n    prepareData(editingElement) {\n        const data = this.getData(editingElement);\n        data.datasets = data.datasets.map((dataset) => {\n            if (dataset.key) {\n                return dataset;\n            }\n            return {\n                ...dataset,\n                key: DATASET_KEY_PREFIX + Date.now(),\n            };\n        });\n        return data;\n    }\n    isPieChart(editingElement) {\n        const isPieChart = this.dependencies.chartOptionPlugin.isPieChart(editingElement);\n        if (!this.domState || this.domState.isPieChart !== isPieChart) {\n            // Pie charts set color on a data cell basis, whereas the\n            // other ones set it on a dataset basis\n            const { backgroundLabel, borderLabel } = this.getColorpickersLabels(isPieChart);\n            this.updateCurrentCell({ backgroundLabel, borderLabel });\n        }\n        return isPieChart;\n    }\n    getColorpickersLabels(isPieChart) {\n        const backgroundLabel = isPieChart ? _t(\"Data Color\") : _t(\"Dataset Color\");\n        const borderLabel = isPieChart ? _t(\"Data Border\") : _t(\"Dataset Border\");\n        return { backgroundLabel, borderLabel };\n    }\n    getColor(color) {\n        return getColor(color, this.window, this.document);\n    }\n    /**\n     * Retrieve the colors already in use in the chart.\n     *\n     * @returns {Set} set of hexadecimal colors\n     */\n    getColorPalette() {\n        const editingElement = this.env.getEditingElement();\n        const data = this.getData(editingElement);\n        const colorSet = new Set();\n        for (const dataset of data.datasets) {\n            if (this.isPieChart(editingElement)) {\n                dataset.backgroundColor.forEach((color) => colorSet.add(this.getColor(color)));\n                dataset.borderColor.forEach((color) => colorSet.add(this.getColor(color)));\n            } else {\n                colorSet.add(this.getColor(dataset.backgroundColor));\n                colorSet.add(this.getColor(dataset.borderColor));\n            }\n        }\n        colorSet.delete(\"\"); // No color, remove to avoid bugs.\n        return colorSet;\n    }\n\n    /**\n     * @param {Object} updatedCellInfo\n     * @param {Number} [updatedCellInfo.dataIndex]\n     * @param {Number} [updatedCellInfo.datasetIndex]\n     * @param {String} [updatedCellInfo.backgroundLabel]\n     * @param {String} [updatedCellInfo.borderLabel]\n     */\n    updateCurrentCell(updatedCellInfo) {\n        for (const key in updatedCellInfo) {\n            this.state.currentCell[key] = updatedCellInfo[key];\n        }\n    }\n\n    /**\n     * Extracts information about the table cell from a table event.\n     *\n     * @param {Event} ev - The event triggered on the table.\n     * @returns {Object} Containing:\n     *   - cellEl: The cell element (td or th) that was interacted.\n     *   - cellSectionEl: The section element (THEAD or TBODY) containing the cell.\n     *   - datasetIndex: The column index of the cell (excluding label column).\n     *   - dataIndex: The row index of the cell (only for TBODY rows).\n     */\n    getCellInfo(ev) {\n        const cellEl = ev.target.closest(\"td, th\");\n        if (cellEl) {\n            const cellRowEl = cellEl.parentElement;\n            const cellSectionEl = cellRowEl.parentElement;\n            const datasetIndex = [...cellRowEl.children].indexOf(cellEl) - 1;\n            let dataIndex;\n            if (cellSectionEl.tagName === \"TBODY\") {\n                dataIndex = [...cellSectionEl.children].indexOf(cellRowEl);\n            }\n            return { cellEl, cellSectionEl, datasetIndex, dataIndex };\n        }\n        return {};\n    }\n\n    isTableButton(target) {\n        return !!target.closest(\n            \".o_builder_matrix_remove_row, .add_row, .o_builder_matrix_remove_col, .add_column\"\n        );\n    }\n    /**\n     * Store in the state the coords of the cell that is currently focused.\n     *\n     * @param {Event} ev\n     */\n    onTableFocusin(ev) {\n        this.onTableMouseover(ev);\n        this.handleCellFocus(ev);\n    }\n    /**\n     * Used to display the corresponding colorpickers.\n     *\n     * @param {Event} ev\n     */\n    handleCellFocus(ev) {\n        if (this.isTableButton(ev.target)) {\n            return;\n        }\n        const { cellEl, cellSectionEl, datasetIndex, dataIndex } = this.getCellInfo(ev);\n        if (!cellEl) {\n            return;\n        }\n        const cellRowEl = cellEl.parentElement;\n        if (cellSectionEl.tagName === \"THEAD\" && datasetIndex !== -1) {\n            this.updateCurrentCell({\n                datasetIndex: this.domState.isPieChart ? null : datasetIndex,\n                dataIndex: this.domState.isPieChart ? null : 0,\n            });\n        }\n        // click on a table inner cell\n        else if (datasetIndex !== -1 && datasetIndex !== cellRowEl.children.length - 2) {\n            this.updateCurrentCell({ datasetIndex, dataIndex });\n        }\n    }\n    /**\n     * Handles the click on table buttons.\n     *\n     * @param {Event} ev - The event triggered on the table cell button.\n     */\n    onButtonCellClick(ev) {\n        if (!this.isTableButton(ev.target)) {\n            return;\n        }\n        const { cellEl, cellSectionEl, datasetIndex, dataIndex } = this.getCellInfo(ev);\n        const isColumnButton = dataIndex === cellSectionEl.children.length - 1;\n        const isRowButton = datasetIndex === cellEl.parentElement.children.length - 2;\n\n        if (isColumnButton) {\n            if (ev.target.classList.contains(\"add_row\")) {\n                this.updateCurrentCell({ datasetIndex: 0, dataIndex });\n            }\n            // if we delete a column with the current cell\n            else if (datasetIndex === this.state.currentCell.datasetIndex) {\n                this.setDefaultState();\n            } else if (datasetIndex < this.state.currentCell.datasetIndex) {\n                this.updateCurrentCell({ datasetIndex: this.state.currentCell.datasetIndex - 1 });\n            }\n            return;\n        }\n\n        if (isRowButton) {\n            if (ev.target.classList.contains(\"add_column\")) {\n                this.updateCurrentCell({ datasetIndex, dataIndex: 0 });\n            }\n            // if we delete a row with the current cell\n            else if (dataIndex === this.state.currentCell.dataIndex) {\n                this.setDefaultState();\n            } else if (dataIndex < this.state.currentCell.dataIndex) {\n                this.updateCurrentCell({ dataIndex: this.state.currentCell.dataIndex - 1 });\n            }\n        }\n    }\n\n    /**\n     * Handles the click on the THEAD (Dataset Labels).\n     *\n     * @param {Event} ev - The event triggered on the thead cell.\n     */\n    onDatasetLabelClick(ev) {\n        const { datasetIndex } = this.getCellInfo(ev);\n        this.updateCurrentCell({\n            datasetIndex: this.domState.isPieChart ? null : datasetIndex,\n            dataIndex: this.domState.isPieChart ? null : 0,\n        });\n    }\n\n    onTableMouseoutOrFocusout(ev) {\n        ev.currentTarget\n            .querySelector(\".o_builder_matrix_remove_col:not(.visually-hidden-focusable)\")\n            ?.classList.add(\"visually-hidden-focusable\");\n        ev.currentTarget\n            .querySelector(\".o_builder_matrix_remove_row:not(.visually-hidden-focusable)\")\n            ?.classList.add(\"visually-hidden-focusable\");\n    }\n    /**\n     * Compute the column that is hovered and show the remove_col button.\n     *\n     * @param {Event} ev\n     */\n    onTableMouseover(ev) {\n        ev.stopPropagation();\n        if (ev.target === ev.currentTarget) {\n            return;\n        }\n        const tableEl = ev.currentTarget;\n        const cellEl = ev.target.closest(\"td, th\");\n        const rowEl = cellEl.closest(\"tr\");\n        const columnIndex = [...rowEl.children].indexOf(cellEl);\n\n        // Remove column: allowed if more than 1 dataset & on dataset columns (\n        // not on the labels column nor the buttons column).\n        if (\n            rowEl.children.length > 3 && // label + value + button\n            columnIndex > 0 &&\n            columnIndex < rowEl.children.length - 1\n        ) {\n            tableEl\n                .querySelectorAll(\".o_builder_matrix_remove_col\")\n                [columnIndex - 1].classList.remove(\"visually-hidden-focusable\");\n        }\n\n        // Remove row: allowed if more than 1 label & on actual data rows (not\n        // on the header row nor the buttons row).\n        if (\n            rowEl.parentElement.children.length > 2 && // value + button\n            cellEl.closest(\"tbody\") &&\n            rowEl.nextElementSibling\n        ) {\n            rowEl\n                .querySelector(\".o_builder_matrix_remove_row\")\n                .classList.remove(\"visually-hidden-focusable\");\n        }\n    }\n}\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { ChartOption, DATASET_KEY_PREFIX, getColor } from \"./chart_option\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\n\n/**\n * @typedef { Object } ChartOptionShared\n * @property { ChartOptionPlugin['isPieChart'] } isPieChart\n */\n\nclass ChartOptionPlugin extends Plugin {\n    static id = \"chartOptionPlugin\";\n    static dependencies = [\"history\"];\n    static shared = [\"isPieChart\"];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [ChartOption],\n        so_content_addition_selector: [\".s_chart\"],\n        builder_actions: {\n            SetChartTypeAction,\n            AddColumnAction,\n            RemoveColumnAction,\n            AddRowAction,\n            RemoveRowAction,\n            UpdateDatasetValueAction,\n            UpdateDatasetLabelAction,\n            UpdateLabelNameAction,\n            setMinMaxAction,\n            ColorChangeAction,\n        },\n    };\n\n    isPieChart(editingElement) {\n        return [\"pie\", \"doughnut\"].includes(editingElement.dataset.type);\n    }\n}\n\nexport class BaseChartAction extends BuilderAction {\n    static id = \"baseChart\";\n    static dependencies = [\"chartOptionPlugin\"];\n    updateDOMData(editingElement, data) {\n        editingElement.dataset.data = JSON.stringify(data);\n    }\n\n    getData(editingElement) {\n        return JSON.parse(editingElement.dataset.data);\n    }\n\n    getMaxValue(editingElement) {\n        const datasets = this.getData(editingElement).datasets;\n        let dataValues;\n        if (!editingElement.dataset.stacked) {\n            dataValues = datasets.flatMap((set) => set.data.map((data) => parseInt(data) || 0));\n        } else {\n            dataValues = datasets.reduce((acc, set) => {\n                const data = set.data.map((data) => parseInt(data) || 0);\n                return acc.map((value, i) => value + data[i]);\n            }, Array(datasets[0].data.length).fill(0));\n        }\n        return Math.ceil(Math.max(...dataValues) / 5) * 5;\n    }\n\n    isPieChart(editingElement) {\n        return this.dependencies.chartOptionPlugin.isPieChart(editingElement);\n    }\n\n    getColor(color) {\n        return getColor(color, this.window, this.document);\n    }\n\n    randomColor() {\n        return (\n            \"#\" + (\"00000\" + ((Math.random() * (1 << 24)) | 0).toString(16)).slice(-6).toUpperCase()\n        );\n    }\n}\n\nexport class SetChartTypeAction extends BaseChartAction {\n    static id = \"setChartType\";\n    isApplied({ editingElement, value }) {\n        return editingElement.dataset.type === value;\n    }\n    apply({ editingElement, value }) {\n        editingElement.dataset.type = value;\n\n        const data = this.getData(editingElement);\n        if (this.isPieChart(editingElement)) {\n            if (typeof data.datasets[0].backgroundColor === \"string\") {\n                data.datasets.forEach((dataset) => {\n                    dataset.backgroundColor = [dataset.backgroundColor];\n                    dataset.borderColor = [dataset.borderColor];\n                    for (let i = 1; i < data.labels.length; i++) {\n                        dataset.backgroundColor.push(this.randomColor());\n                        dataset.borderColor.push(\"\");\n                    }\n                });\n            }\n        } else if (Array.isArray(data.datasets[0].backgroundColor)) {\n            data.datasets.forEach((dataset) => {\n                dataset.backgroundColor = dataset.backgroundColor[0];\n                dataset.borderColor = dataset.borderColor[0];\n            });\n        }\n        this.updateDOMData(editingElement, data);\n    }\n}\nexport class AddColumnAction extends BaseChartAction {\n    static id = \"addColumn\";\n    apply({ editingElement }) {\n        const data = this.getData(editingElement);\n        const fillDatasetArray = (value) => Array(data.labels.length).fill(value);\n\n        const newDataset = {\n            key: DATASET_KEY_PREFIX + Date.now(),\n            label: \"\",\n            data: fillDatasetArray(0),\n            backgroundColor: this.isPieChart(editingElement)\n                ? data.labels.map(() => this.randomColor())\n                : this.randomColor(),\n            borderColor: this.isPieChart(editingElement) ? fillDatasetArray(\"\") : \"\",\n        };\n        data.datasets.push(newDataset);\n        this.updateDOMData(editingElement, data);\n    }\n}\nexport class RemoveColumnAction extends BaseChartAction {\n    static id = \"removeColumn\";\n    apply({ editingElement, params: { mainParam: key } }) {\n        const data = this.getData(editingElement);\n        const toRemoveIndex = data.datasets.findIndex((dataset) => dataset.key === key);\n        data.datasets.splice(toRemoveIndex, 1);\n        this.updateDOMData(editingElement, data);\n    }\n}\nexport class AddRowAction extends BaseChartAction {\n    static id = \"addRow\";\n    apply({ editingElement }) {\n        const data = this.getData(editingElement);\n        data.labels.push(\"\");\n        data.datasets.forEach((dataset) => {\n            dataset.data.push(0);\n            if (this.isPieChart(editingElement)) {\n                dataset.backgroundColor.push(this.randomColor());\n                dataset.borderColor.push(\"\");\n            }\n        });\n        this.updateDOMData(editingElement, data);\n    }\n}\nexport class RemoveRowAction extends BaseChartAction {\n    static id = \"removeRow\";\n    apply({ editingElement, params: { mainParam: labelIndex } }) {\n        const data = this.getData(editingElement);\n        data.labels.splice(labelIndex, 1);\n        data.datasets.forEach((dataset) => {\n            dataset.data.splice(labelIndex, 1);\n            if (this.isPieChart(editingElement)) {\n                dataset.backgroundColor.splice(labelIndex, 1);\n                dataset.borderColor.splice(labelIndex, 1);\n            }\n        });\n        this.updateDOMData(editingElement, data);\n    }\n}\nexport class UpdateDatasetValueAction extends BaseChartAction {\n    static id = \"updateDatasetValue\";\n    getValue({ editingElement, params: { datasetKey, valueIndex } }) {\n        const data = this.getData(editingElement);\n        const targetDataset = data.datasets.find((dataset) => dataset.key === datasetKey);\n        return targetDataset?.data[valueIndex] || 0;\n    }\n    apply({ editingElement, value, params: { datasetKey, valueIndex } }) {\n        const data = this.getData(editingElement);\n        const targetDataset = data.datasets.find((dataset) => dataset.key === datasetKey);\n        targetDataset.data[valueIndex] = value;\n        this.updateDOMData(editingElement, data);\n    }\n}\nexport class UpdateDatasetLabelAction extends BaseChartAction {\n    static id = \"updateDatasetLabel\";\n    getValue({ editingElement, params: { mainParam: datasetKey } }) {\n        const data = this.getData(editingElement);\n        const targetDataset = data.datasets.find((dataset) => dataset.key === datasetKey);\n        return targetDataset?.label;\n    }\n    apply({ editingElement, value, params: { mainParam: datasetKey } }) {\n        const data = this.getData(editingElement);\n        const targetDataset = data.datasets.find((dataset) => dataset.key === datasetKey);\n        targetDataset.label = value;\n        this.updateDOMData(editingElement, data);\n    }\n}\n\nexport class UpdateLabelNameAction extends BaseChartAction {\n    static id = \"updateLabelName\";\n    getValue({ editingElement, params: { mainParam: labelIndex } }) {\n        const data = this.getData(editingElement);\n        return data.labels[labelIndex];\n    }\n    apply({ editingElement, value, params: { mainParam: labelIndex } }) {\n        const data = this.getData(editingElement);\n        data.labels[labelIndex] = value;\n        this.updateDOMData(editingElement, data);\n    }\n}\nexport class setMinMaxAction extends BaseChartAction {\n    static id = \"setMinMax\";\n    getValue({ editingElement, params: { mainParam: type } }) {\n        if (type === \"min\") {\n            return parseInt(editingElement.dataset.ticksMin) || \"\";\n        }\n        if (type === \"max\") {\n            return parseInt(editingElement.dataset.ticksMax) || \"\";\n        }\n    }\n    apply({ editingElement, value, params: { mainParam: type } }) {\n        let minValue, maxValue;\n        let noMin = false;\n        let noMax = false;\n        if (type === \"min\") {\n            minValue = parseInt(value);\n            maxValue = parseInt(editingElement.dataset.ticksMax);\n        }\n        if (type === \"max\") {\n            maxValue = parseInt(value);\n            minValue = parseInt(editingElement.dataset.ticksMin);\n        }\n        if (isNaN(minValue)) {\n            noMin = true;\n            minValue = 0;\n        }\n\n        if (!isNaN(maxValue)) {\n            if (maxValue < minValue) {\n                [minValue, maxValue] = [maxValue, minValue];\n                [noMin, noMax] = [noMax, noMin];\n            } else if (maxValue === minValue) {\n                minValue = minValue < 0 ? 2 * minValue : 0;\n                maxValue = minValue < 0 ? 0 : 2 * maxValue;\n            }\n        } else {\n            noMax = true;\n            maxValue = this.getMaxValue(editingElement);\n            // When max value is not given and min value is greater\n            // than chart data values\n            if (minValue > maxValue) {\n                maxValue = minValue;\n                [noMin, noMax] = [noMax, noMin];\n            }\n        }\n\n        if (noMin) {\n            delete editingElement.dataset.ticksMin;\n        } else {\n            editingElement.dataset.ticksMin = minValue;\n        }\n        if (noMax) {\n            delete editingElement.dataset.ticksMax;\n        } else {\n            editingElement.dataset.ticksMax = maxValue;\n        }\n    }\n}\nexport class ColorChangeAction extends BaseChartAction {\n    static id = \"colorChange\";\n    getValue({ editingElement, params: { type, datasetIndex, dataIndex } }) {\n        const data = this.getData(editingElement);\n        if (this.isPieChart(editingElement)) {\n            // TODO: shouldn't getColor be done directly in BuilderColorPicker?\n            return this.getColor(data.datasets[datasetIndex]?.[type][dataIndex]);\n        } else {\n            return this.getColor(data.datasets[datasetIndex]?.[type]);\n        }\n    }\n    apply({ editingElement, value, params: { type, datasetIndex, dataIndex } }) {\n        value = value.replace(\"var(--\", \"\").replace(\")\", \"\");\n        const data = this.getData(editingElement);\n        if (this.isPieChart(editingElement)) {\n            data.datasets[datasetIndex][type][dataIndex] = value;\n        } else {\n            data.datasets[datasetIndex][type] = value;\n        }\n        this.updateDOMData(editingElement, data);\n    }\n}\n\nregistry.category(\"website-plugins\").add(ChartOptionPlugin.id, ChartOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nconst mainObjectRe = /website\\.controller\\.page\\(((\\d+,?)*)\\)/;\n\nexport class ControllerPageListingLayoutOption extends BaseOptionComponent {\n    static template = \"website.ControllerPageListingLayoutOption\";\n    static selector = \".listing_layout_switcher\";\n    static editableOnly = false;\n    static title = _t(\"Layout\");\n    static groups = [\"website.group_website_designer\"];\n}\n\nclass ControllerPageListingLayoutOptionPlugin extends Plugin {\n    static id = \"controllerPageListingLayoutOption\";\n    static dependencies = [\"builderActions\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [ControllerPageListingLayoutOption],\n        builder_actions: {\n            ListingLayoutAction,\n        },\n    };\n}\n\nexport class ListingLayoutAction extends BuilderAction {\n    static id = \"listingLayout\";\n    setup() {\n        this.reload = {};\n        this.layout = undefined;\n        this.resIds = undefined;\n    }\n    async prepare() {\n        const mainObjectRepr = this.document.documentElement.getAttribute(\"data-main-object\");\n        const match = mainObjectRe.exec(mainObjectRepr);\n        if (match && match[1]) {\n            this.resIds = match[1].split(\",\").flatMap((e) => {\n                if (!e) {\n                    return [];\n                }\n                const id = parseInt(e);\n                return id ? [id] : [];\n            });\n        }\n        const results = await this.services.orm.read(\"website.controller.page\", this.resIds, [\n            \"default_layout\",\n        ]);\n        this.layout = results[0][\"default_layout\"];\n    }\n    getValue() {\n        return this.layout;\n    }\n    isApplied({ value }) {\n        return this.layout === value;\n    }\n    async apply({ editingElement: el, value }) {\n        const params = {\n            layout_mode: value,\n            view_id: el.dataset.viewId,\n        };\n        // Save the default layout display, and set the layout for the current user\n        await Promise.all([\n            this.services.orm.write(\"website.controller.page\", this.resIds, {\n                default_layout: value,\n            }),\n            rpc(\"/website/save_session_layout_mode\", params),\n        ]);\n    }\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(ControllerPageListingLayoutOptionPlugin.id, ControllerPageListingLayoutOptionPlugin);\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\n/**\n * @typedef { Object } CookiesBarOptionShared\n * @property { CookiesBarOptionPlugin['getSavedSelectors'] } getSavedSelectors\n */\n\nexport class CookiesBarOption extends BaseOptionComponent {\n    static template = \"website.CookiesBarOption\";\n    static selector = \"#website_cookies_bar\";\n    static applyTo = \".modal\";\n}\nclass CookiesBarOptionPlugin extends Plugin {\n    static id = \"CookiesBarOptionPlugin\";\n    static shared = [\"getSavedSelectors\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [CookiesBarOption],\n        builder_actions: {\n            SelectLayoutAction,\n        },\n    };\n\n    setup() {\n        this.savedSelectors = {};\n    }\n\n    getSavedSelectors() {\n        return this.savedSelectors;\n    }\n}\n\nexport class SelectLayoutAction extends BuilderAction {\n    static id = \"selectLayout\";\n    static dependencies = [\"CookiesBarOptionPlugin\"];\n    apply({ editingElement, value: layout }) {\n        const savedSelectors = this.dependencies.CookiesBarOptionPlugin.getSavedSelectors();\n        const templateEl = renderToElement(`website.cookies_bar.${layout}`, {\n            websiteId: this.services.website.currentWebsite.id,\n        });\n        const contentEl = editingElement.querySelector(\".modal-content\");\n\n        // The selectors' order is significant since some selectors\n        // may be nested within others, and we want to preserve the\n        // nested ones.\n        // For instance, in the case of '.o_cookies_bar_text_policy'\n        // nested inside '.o_cookies_bar_text_secondary', the parent\n        // selector should be copied first, followed by the child\n        // selector to ensure that the content of the nested\n        // selector is not overwritten.\n        const selectorsToKeep = [\n            \".o_cookies_bar_text_button\",\n            \".o_cookies_bar_text_button_essential\",\n            \".o_cookies_bar_text_title\",\n            \".o_cookies_bar_text_primary\",\n            \".o_cookies_bar_text_secondary\",\n            \".o_cookies_bar_text_policy\",\n        ];\n\n        for (const selector of selectorsToKeep) {\n            const currentLayoutEls = contentEl.querySelector(selector)?.childNodes;\n            const newLayoutEl = templateEl.querySelector(selector);\n            if (currentLayoutEls && currentLayoutEls.length) {\n                // Save value before change, eg 'title' is not\n                // inside the 'discrete' template but we want to\n                // preserve it in case we select another layout\n                // later\n                savedSelectors[selector] = [...currentLayoutEls];\n            }\n            const savedSelector = savedSelectors[selector];\n            if (newLayoutEl && savedSelector?.length) {\n                newLayoutEl.replaceChildren(...savedSelector);\n            }\n        }\n\n        contentEl.replaceChildren(templateEl);\n\n        switch (layout) {\n            case \"discrete\":\n            case \"classic\":\n                editingElement.classList.add(\"s_popup_bottom\");\n                this.getDialogEl(editingElement).classList.add(\"s_popup_size_full\");\n                break;\n            case \"popup\":\n                editingElement.classList.add(\"s_popup_middle\");\n                break;\n        }\n    }\n    clean({ editingElement }) {\n        // See popup_option.xml > Position option\n        const positionClasses = [\"s_popup_top\", \"s_popup_middle\", \"s_popup_bottom\"];\n        // See popup_option.xml > Size option\n        const sizeClasses = [\"modal-sm\", \"modal-lg\", \"modal-xl\", \"s_popup_size_full\"];\n        editingElement.classList.remove(...positionClasses);\n        this.getDialogEl(editingElement).classList.remove(...sizeClasses);\n    }\n    getDialogEl(editingElement) {\n        return editingElement.querySelector(\".modal-dialog\");\n    }\n}\n\nregistry.category(\"website-plugins\").add(CookiesBarOptionPlugin.id, CookiesBarOptionPlugin);\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { before, SNIPPET_SPECIFIC_END } from \"@html_builder/utils/option_sequence\";\nimport { getElementsWithOption } from \"@html_builder/utils/utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\nexport class CountdownOption extends BaseOptionComponent {\n    static template = \"website.CountdownOption\";\n    static selector = \".s_countdown\";\n    static cleanForSave = (editingEl) => {\n        editingEl.classList.remove(\"s_countdown_enable_preview\");\n    };\n}\n\nclass CountdownOptionPlugin extends Plugin {\n    static id = \"CountdownOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(before(SNIPPET_SPECIFIC_END), CountdownOption)],\n        so_content_addition_selector: [\".s_countdown\"],\n        builder_actions: {\n            // TODO AGAU: update after merging generalized restart interactions\n            //  remove this and xml BuilderContext\n            ReloadCountdownAction,\n            SetEndActionAction,\n            PreviewEndMessageAction,\n            SetLayoutAction,\n        },\n        on_cloned_handlers: ({ cloneEl }) => {\n            const countdownEls = getElementsWithOption(cloneEl, \".s_countdown\");\n            for (const countdownEl of countdownEls) {\n                countdownEl.classList.remove(\"s_countdown_enable_preview\");\n            }\n        },\n    };\n}\n\nexport class BaseCountdownAction extends BuilderAction {\n    static id = \"baseCountdown\";\n    /**\n     * Used to preserve modified end messages through end action changes. This\n     * allows the user to test options without losing their progress while in\n     * between saves.\n     *\n     * @type {WeakMap<Element, Element>}\n     */\n    editingElEndMessages = new WeakMap();\n\n    cleanForSave(editingEl) {\n        return CountdownOption.cleanForSave(editingEl);\n    }\n\n    setEndAction({ editingElement, value }) {\n        editingElement.dataset.endAction = value;\n        const endMessageEl = editingElement.querySelector(\".s_countdown_end_message\");\n\n        // Only hide countdown in one case\n        editingElement.classList.toggle(\"hide-countdown\", value === \"message_no_countdown\");\n\n        // Only have redirect url attribute in one case\n        if (value === \"redirect\") {\n            editingElement.dataset.redirectUrl = \"\";\n        } else {\n            delete editingElement.dataset.redirectUrl;\n        }\n\n        if (value === \"message\" || value === \"message_no_countdown\") {\n            if (!endMessageEl) {\n                const existingEndMessage = this.editingElEndMessages.get(editingElement);\n                editingElement.appendChild(\n                    existingEndMessage || renderToElement(\"website.s_countdown.end_message\")\n                );\n            }\n        } else {\n            endMessageEl?.remove();\n            this.editingElEndMessages.set(editingElement, endMessageEl);\n            // Reset end message preview to avoid countdown staying hidden\n            this.toggleEndMessagePreview(editingElement, false);\n        }\n    }\n\n    isEndActionApplied({ editingElement, value }) {\n        return editingElement.dataset.endAction === value;\n    }\n\n    setLayout({ editingElement, value }) {\n        switch (value) {\n            case \"circle\":\n                editingElement.dataset.progressBarStyle = \"disappear\";\n                editingElement.dataset.progressBarWeight = \"thin\";\n                editingElement.dataset.layoutBackground = \"none\";\n                break;\n            case \"boxes\":\n                editingElement.dataset.progressBarStyle = \"none\";\n                editingElement.dataset.layoutBackground = \"plain\";\n                break;\n            case \"clean\":\n                editingElement.dataset.progressBarStyle = \"none\";\n                editingElement.dataset.layoutBackground = \"none\";\n                break;\n            case \"text\":\n                editingElement.dataset.progressBarStyle = \"none\";\n                editingElement.dataset.layoutBackground = \"none\";\n                break;\n        }\n        editingElement.dataset.layout = value;\n    }\n\n    isLayoutApplied({ editingElement, value }) {\n        return editingElement.dataset.layout === value;\n    }\n\n    isEndMessagePreviewed({ editingElement }) {\n        return !!editingElement?.classList.contains(\"s_countdown_enable_preview\");\n    }\n\n    toggleEndMessagePreview(editingElement, doShow) {\n        editingElement?.classList.toggle(\"s_countdown_enable_preview\", doShow === true);\n    }\n}\n\n// TODO AGAU: update after merging generalized restart interactions\n//  remove this and xml BuilderContext\nexport class ReloadCountdownAction extends BaseCountdownAction {\n    static id = \"reloadCountdown\";\n    apply({ editingElement }) {\n        return this.dispatchTo(\"update_interactions\", editingElement);\n    }\n}\n\nexport class SetEndActionAction extends BaseCountdownAction {\n    static id = \"setEndAction\";\n    apply(context) {\n        return this.setEndAction(context);\n    }\n    isApplied(context) {\n        return this.isEndActionApplied(context);\n    }\n}\n\nexport class PreviewEndMessageAction extends BaseCountdownAction {\n    static id = \"previewEndMessage\";\n    static dependencies = [\"builderOptions\"];\n    apply({ editingElement }) {\n        this.toggleEndMessagePreview(editingElement, true);\n    }\n    clean({ editingElement }) {\n        this.toggleEndMessagePreview(editingElement, false);\n        // Activate the countdown options, to not stay on the message preview\n        // ones if they were active.\n        this.dependencies.builderOptions.setNextTarget(editingElement);\n    }\n    isApplied(context) {\n        return this.isEndMessagePreviewed(context);\n    }\n}\n\nexport class SetLayoutAction extends BaseCountdownAction {\n    static id = \"setLayout\";\n    apply(context) {\n        return this.setLayout(context);\n    }\n    isApplied(context) {\n        return this.isLayoutApplied(context);\n    }\n}\nregistry.category(\"website-plugins\").add(CountdownOptionPlugin.id, CountdownOptionPlugin);\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class CoverPropertiesOption extends BaseOptionComponent {\n    static template = \"website.CoverPropertiesOption\";\n    static selector = \".o_record_cover_container\";\n    static editableOnly = false;\n\n    setup() {\n        super.setup();\n        this.state = useDomState((editingElement) => ({\n            useTextAlign: editingElement.dataset.use_text_align === \"True\",\n            useSize: editingElement.dataset.use_size === \"True\",\n        }));\n        this.coverSizeClasses = Object.keys(coverSizeClassLabels);\n    }\n\n    coverSizeLabel(className) {\n        return coverSizeClassLabels[className];\n    }\n}\n\nexport const coverSizeClassLabels = {\n    o_full_screen_height: _t(\"Full Screen\"),\n    o_half_screen_height: _t(\"Half Screen\"),\n    cover_auto: _t(\"Fit text\"),\n};\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { CoverPropertiesOption } from \"@website/builder/plugins/options/cover_properties_option\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { COVER_PROPERTIES } from \"@website/builder/option_sequence\";\nimport { coverSizeClassLabels } from \"./cover_properties_option\";\n\nclass CoverPropertiesOptionPlugin extends Plugin {\n    static id = \"coverPropertiesOption\";\n    static dependencies = [\"builderActions\", \"media\", \"imagePostProcess\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(COVER_PROPERTIES, CoverPropertiesOption)],\n        builder_actions: {\n            SetCoverBackgroundAction,\n            MarkCoverPropertiesToBeSavedAction,\n        },\n        savable_selectors: \"#wrapwrap .o_record_cover_container[data-res-model]\",\n        before_save_handlers: this.savePendingBackgroundImage.bind(this),\n        save_element_handlers: this.saveCoverProperties.bind(this),\n    };\n\n    async savePendingBackgroundImage(editableEl = this.editable) {\n        for (const coverEl of editableEl.querySelectorAll(\".o_record_cover_container\")) {\n            const bgEl = coverEl.querySelector(\".o_record_cover_image\");\n            const bgImage = bgEl?.style.backgroundImage;\n            if (bgImage && bgEl.classList.contains(\"o_b64_cover_image_to_save\")) {\n                const resModel = coverEl.dataset.resModel;\n                const resID = Number(coverEl.dataset.resId);\n                if (!resModel || !resID) {\n                    throw new Error(\"There should be a model and id associated to the cover\");\n                }\n\n                // Checks if the image is in base64 format for RPC call. Relying\n                // only on the presence of the class \"o_b64_cover_image_to_save\" is not\n                // robust enough.\n                const groups = bgImage.match(\n                    /url\\(\"data:(?<mimetype>.*);base64,(?<imageData>.*)\"\\)/\n                )?.groups;\n                if (groups?.imageData) {\n                    const modelName = await this.services.website.getUserModelName(resModel);\n                    const recordNameEl = bgEl\n                        .closest(\"body\")\n                        .querySelector(\n                            `[data-oe-model=\"${resModel}\"][data-oe-id=\"${resID}\"][data-oe-field=\"name\"]`\n                        );\n                    const recordName = recordNameEl\n                        ? `'${recordNameEl.textContent.replaceAll(\"/\", \"\")}'`\n                        : resID;\n                    const attachment = await rpc(\"/web_editor/attachment/add_data\", {\n                        name: `${modelName} ${recordName} cover image.${\n                            groups.mimetype.split(\"/\")[1]\n                        }`,\n                        data: groups.imageData,\n                        is_image: true,\n                        res_model: \"ir.ui.view\",\n                    });\n                    bgEl.style.backgroundImage = `url(${attachment.image_src})`;\n                }\n                bgEl.classList.remove(\"o_b64_cover_image_to_save\");\n            }\n        }\n    }\n\n    saveCoverProperties(el) {\n        if (!el.dataset.coverPropertiesToBeSaved) {\n            return;\n        }\n        delete el.dataset.coverPropertiesToBeSaved;\n\n        const resModel = el.dataset.resModel;\n        const resID = Number(el.dataset.resId);\n\n        if (!resModel || !resID) {\n            throw new Error(\"There should be a model and id associated to the cover\");\n        }\n\n        return this.services.orm.write(resModel, [resID], {\n            cover_properties: JSON.stringify(this.readCoverPoperties(el)),\n        });\n    }\n\n    readCoverPoperties(el) {\n        const coverProperties = {};\n        const bg = el.querySelector(\".o_record_cover_image\")?.style.backgroundImage || \"\";\n        coverProperties[\"background-image\"] = bg;\n\n        // TODO: `o_record_has_cover` should be handled using model field, not\n        // resize_class to avoid all of this.\n        let coverClass = Object.keys(coverSizeClassLabels).find((e) => el.classList.contains(e));\n        if (bg && bg !== \"none\") {\n            coverClass += \" o_record_has_cover\";\n        }\n        coverProperties.resize_class = coverClass;\n\n        coverProperties.text_align_class =\n            [\"text-center\", \"text-end\"].find((e) => el.classList.contains(e)) || \"\";\n\n        coverProperties.opacity = el.querySelector(\".o_record_cover_filter\")?.style.opacity || 0.0;\n\n        coverProperties.background_color_class = [...el.classList.values()]\n            .filter((e) => e.startsWith(\"bg-\") || e.startsWith(\"o_cc\"))\n            .join(\" \");\n        if (el.style.backgroundImage) {\n            coverProperties.background_color_style = `background-color: rgba(0, 0, 0, 0); background-image: ${el.style.backgroundImage};`;\n        } else if (el.style.backgroundColor) {\n            coverProperties.background_color_style = `background-color: ${el.style.backgroundColor};`;\n        } else {\n            coverProperties.background_color_style = \"\";\n        }\n        return coverProperties;\n    }\n}\n\nexport class BaseCoverPropertiesAction extends BuilderAction {\n    static id = \"baseCoverProperties\";\n    markCoverPropertiesToBeSaved({ editingElement }) {\n        editingElement.closest(\".o_record_cover_container\").dataset.coverPropertiesToBeSaved = true;\n    }\n}\n\nexport class SetCoverBackgroundAction extends BaseCoverPropertiesAction {\n    static id = \"setCoverBackground\";\n    static dependencies = [\"builderActions\", \"media\"];\n    setup() {\n        this.classAction = this.dependencies.builderActions.getAction(\"classAction\");\n        this.styleAction = this.dependencies.builderActions.getAction(\"styleAction\");\n    }\n    load({ params: { mainParam: setBackground } }) {\n        if (!setBackground) {\n            return;\n        }\n        let resultPromise;\n        return this.dependencies.media\n            .openMediaDialog({\n                onlyImages: true,\n                save: (imageEl) => {\n                    resultPromise = (async () => {\n                        const b64ToSave = imageEl.getAttribute(\"src\").startsWith(\"data:\");\n                        return { imageSrc: imageEl.getAttribute(\"src\"), b64ToSave };\n                    })();\n                },\n            })\n            .then(() => resultPromise || { cancel: true });\n    }\n\n    isApplied({ editingElement, params: { mainParam: setBackground } }) {\n        const bg = editingElement.querySelector(\".o_record_cover_image\").style.backgroundImage;\n        return !setBackground === (!bg || bg === \"none\");\n    }\n    apply({ editingElement, loadResult: { imageSrc, b64ToSave, cancel } = {} }) {\n        if (cancel) {\n            return;\n        }\n        (imageSrc ? this.classAction.apply : this.classAction.clean)({\n            editingElement,\n            params: { mainParam: \"o_record_has_cover\" },\n        });\n\n        const bgEl = editingElement.querySelector(\".o_record_cover_image\");\n\n        (b64ToSave ? this.classAction.apply : this.classAction.clean)({\n            editingElement: bgEl,\n            params: { mainParam: \"o_b64_cover_image_to_save\" },\n        });\n\n        this.styleAction.apply({\n            editingElement: bgEl,\n            params: { mainParam: \"background-image\" },\n            value: imageSrc ? `url('${imageSrc}')` : \"\",\n        });\n\n        editingElement.closest(\".o_record_cover_container\").dataset.coverPropertiesToBeSaved = true;\n    }\n}\nexport class MarkCoverPropertiesToBeSavedAction extends BaseCoverPropertiesAction {\n    static id = \"markCoverPropertiesToBeSaved\";\n    apply({ editingElement }) {\n        editingElement.closest(\".o_record_cover_container\").dataset.coverPropertiesToBeSaved = true;\n    }\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(CoverPropertiesOptionPlugin.id, CoverPropertiesOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { useDynamicSnippetOption } from \"./dynamic_snippet_hook\";\n\nexport class DynamicSnippetCarouselOption extends BaseOptionComponent {\n    static template = \"website.DynamicSnippetCarouselOption\";\n    static dependencies = [\"dynamicSnippetCarouselOption\"];\n    static selector = \".s_dynamic_snippet_carousel\";\n\n    setup() {\n        super.setup();\n        const { getModelNameFilter } = this.dependencies.dynamicSnippetCarouselOption;\n        this.dynamicOptionParams = useDynamicSnippetOption(getModelNameFilter());\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\nimport { DynamicSnippetCarouselOption } from \"./dynamic_snippet_carousel_option\";\nimport { DYNAMIC_SNIPPET, setDatasetIfUndefined } from \"./dynamic_snippet_option_plugin\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\n\n/**\n * @typedef { Object } DynamicSnippetCarouselOptionShared\n * @property { DynamicSnippetCarouselOptionPlugin['setOptionsDefaultValues'] } setOptionsDefaultValues\n * @property { DynamicSnippetCarouselOptionPlugin['updateTemplateSnippetCarousel'] } updateTemplateSnippetCarousel\n * @property { DynamicSnippetCarouselOptionPlugin['getModelNameFilter'] } getModelNameFilter\n */\n\nexport const DYNAMIC_SNIPPET_CAROUSEL = DYNAMIC_SNIPPET;\n\nclass DynamicSnippetCarouselOptionPlugin extends Plugin {\n    static id = \"dynamicSnippetCarouselOption\";\n    static shared = [\n        \"setOptionsDefaultValues\",\n        \"updateTemplateSnippetCarousel\",\n        \"getModelNameFilter\",\n    ];\n    static dependencies = [\"dynamicSnippetOption\"];\n    modelNameFilter = \"\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_actions: {\n            SetCarouselSliderSpeedAction,\n        },\n        builder_options: withSequence(DYNAMIC_SNIPPET_CAROUSEL, DynamicSnippetCarouselOption),\n        dynamic_snippet_template_updated: this.onTemplateUpdated.bind(this),\n        on_snippet_dropped_handlers: this.onSnippetDropped.bind(this),\n    };\n    getModelNameFilter() {\n        return this.modelNameFilter;\n    }\n    onTemplateUpdated({ el, template }) {\n        if (el.matches(DynamicSnippetCarouselOption.selector)) {\n            this.updateTemplateSnippetCarousel(el, template);\n        }\n    }\n    updateTemplateSnippetCarousel(el, template) {\n        if (template.rowPerSlide) {\n            el.dataset.rowPerSlide = template.rowPerSlide;\n        } else {\n            delete el.dataset.rowPerSlide;\n        }\n        if (template.arrowPosition) {\n            el.dataset.arrowPosition = template.arrowPosition;\n        } else {\n            delete el.dataset.arrowPosition;\n        }\n    }\n    async onSnippetDropped({ snippetEl }) {\n        if (snippetEl.matches(DynamicSnippetCarouselOption.selector)) {\n            await this.setOptionsDefaultValues(snippetEl, this.modelNameFilter);\n        }\n    }\n    async setOptionsDefaultValues(snippetEl, modelNameFilter, contextualFilterDomain = []) {\n        await this.dependencies.dynamicSnippetOption.setOptionsDefaultValues(\n            snippetEl,\n            modelNameFilter,\n            contextualFilterDomain\n        );\n        setDatasetIfUndefined(snippetEl, \"carouselInterval\", \"5000\");\n    }\n}\n\nexport class SetCarouselSliderSpeedAction extends BuilderAction {\n    static id = \"setCarouselSliderSpeed\";\n    apply({ editingElement, value }) {\n        editingElement.dataset.carouselInterval = value * 1000;\n    }\n    getValue({ editingElement }) {\n        return editingElement.dataset.carouselInterval === undefined\n            ? undefined\n            : editingElement.dataset.carouselInterval / 1000;\n    }\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(DynamicSnippetCarouselOptionPlugin.id, DynamicSnippetCarouselOptionPlugin);\n", "import { useDomState } from \"@html_builder/core/utils\";\nimport { onWillStart, useEnv } from \"@odoo/owl\";\n\nexport function useDynamicSnippetOption(modelNameFilter, contextualFilterDomain = []) {\n    const env = useEnv();\n    onWillStart(async () => {\n        await fetchDynamicFiltersAndTemplates();\n        // TODO: For now, a snippet is considered in \"single mode\" only when one\n        // record is selected and at least one \"single template\" is available\n        // for its model (which requires templates to be already fetched...).\n        // The snippet automatically switches to multi-record templates but with\n        // one item if it has no layouts for single mode. This can be improved\n        // once single templates are added for all dynamic snippet models, and\n        // the selection of one record will be enough.\n        domState.isSingleMode = dynamicSnippetUtils.isSingleModeSnippet(domState);\n    });\n    const dynamicFilterTemplates = {};\n    // Common functions to handle dynamic snippets filters & templates...\n    const dynamicSnippetUtils = env.editor.shared.dynamicSnippetOption;\n    const dynamicFilters = {};\n    const domState = useDomState((editingElement) => ({\n        filterId: editingElement.dataset.filterId,\n        snippetModel: editingElement.dataset.snippetModel || modelNameFilter,\n        numberOfRecords: parseInt(editingElement.dataset.numberOfRecords),\n        templateKey: editingElement.dataset.templateKey,\n        isSingleMode: dynamicSnippetUtils.isSingleModeSnippet(editingElement.dataset),\n    }));\n\n    async function fetchDynamicFiltersAndTemplates() {\n        const fetchedDynamicFilters = await dynamicSnippetUtils.fetchDynamicFilters({\n            model_name: modelNameFilter,\n            search_domain: contextualFilterDomain,\n        });\n        if (!fetchedDynamicFilters.length) {\n            // Additional modules are needed for dynamic filters to be defined.\n            return;\n        }\n        const uniqueModelName = new Set();\n        for (const dynamicFilter of fetchedDynamicFilters) {\n            dynamicFilters[dynamicFilter.id] = dynamicFilter;\n            uniqueModelName.add(dynamicFilter.model_name);\n        }\n        const fetchedDynamicFilterTemplates =\n            await dynamicSnippetUtils.fetchDynamicSnippetTemplates(modelNameFilter);\n        for (const dynamicFilterTemplate of fetchedDynamicFilterTemplates) {\n            dynamicFilterTemplates[dynamicFilterTemplate.key] = dynamicFilterTemplate;\n        }\n        const defaultTemplatePerModel = {};\n        for (const modelName of uniqueModelName) {\n            for (const template of fetchedDynamicFilterTemplates) {\n                if (dynamicSnippetUtils.isModelSnippetTemplate(template.key, modelName)) {\n                    defaultTemplatePerModel[modelName] = template;\n                    break;\n                }\n            }\n        }\n        for (const dynamicFilter of fetchedDynamicFilters) {\n            dynamicFilter.defaultTemplate = defaultTemplatePerModel[dynamicFilter.model_name];\n        }\n    }\n    function getFilteredTemplates() {\n        if (!Object.values(dynamicFilterTemplates).length) {\n            return [];\n        }\n        const snippetModel = domState.snippetModel || dynamicFilters[domState.filterId].model_name;\n        return Object.values(dynamicFilterTemplates).filter(({ key }) => {\n            const isModelTemplate = dynamicSnippetUtils.isModelSnippetTemplate(key, snippetModel);\n            const isSingleModeTemplate = dynamicSnippetUtils.isSingleModeSnippetTemplate(key);\n            return (\n                isModelTemplate &&\n                (domState.isSingleMode ? isSingleModeTemplate : !isSingleModeTemplate)\n            );\n        });\n    }\n    function showFilterOption() {\n        return !domState.isSingleMode && Object.values(dynamicFilters).length > 1;\n    }\n\n    return {\n        dynamicFilters,\n        domState,\n        getFilteredTemplates,\n        showFilterOption,\n        ...dynamicSnippetUtils,\n    };\n}\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { useDynamicSnippetOption } from \"./dynamic_snippet_hook\";\n\nexport class DynamicSnippetOption extends BaseOptionComponent {\n    static template = \"website.DynamicSnippetOption\";\n    static dependencies = [\"dynamicSnippetOption\"];\n    static selector = \".s_dynamic_snippet\";\n    static props = {\n        slots: { type: Object, optional: true },\n    };\n\n    setup() {\n        super.setup();\n        const { getModelNameFilter } = this.dependencies.dynamicSnippetOption;\n        // Specify model name in subclasses to filter the list of available\n        // model record filters. Indicates that some current options are a\n        // default selection.\n        this.dynamicOptionParams = useDynamicSnippetOption(getModelNameFilter());\n    }\n}\n", "import { SNIPPET_SPECIFIC_END } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { Cache } from \"@web/core/utils/cache\";\nimport { DynamicSnippetOption } from \"./dynamic_snippet_option\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\n\n/**\n * @typedef {object} Template\n * @property {string} arrowPosition\n * @property {string} columnClasses\n * @property {string} containerClasses\n * @property {string} contentClasses\n * @property {string} extraClasses\n * @property {string} extraSnippetClasses\n * @property {string} key\n * @property {string} numberOfElements\n * @property {string} numberOfElementsSmallDevices\n * @property {string} numberOfRecords\n * @property {string} rowPerSlide\n * @property {string} thumb\n */\n\n/**\n * @typedef { Object } DynamicSnippetOptionShared\n * @property { DynamicSnippetOptionPlugin['fetchDynamicFilters'] } fetchDynamicFilters\n * @property { DynamicSnippetOptionPlugin['fetchDynamicSnippetTemplates'] } fetchDynamicSnippetTemplates\n * @property { DynamicSnippetOptionPlugin['getDefaultSnippetFilterId'] } getDefaultSnippetFilterId\n * @property { DynamicSnippetOptionPlugin['getDefaultSnippetRecordId'] } getDefaultSnippetRecordId\n * @property { DynamicSnippetOptionPlugin['getDefaultSnippetTemplate'] } getDefaultSnippetTemplate\n * @property { DynamicSnippetOptionPlugin['getSnippetModelName'] } getSnippetModelName\n * @property { DynamicSnippetOptionPlugin['getSnippetTitleClasses'] } getSnippetTitleClasses\n * @property { DynamicSnippetOptionPlugin['getTemplateByKey'] } getTemplateByKey\n * @property { DynamicSnippetOptionPlugin['isModelSnippetTemplate'] } isModelSnippetTemplate\n * @property { DynamicSnippetOptionPlugin['isSingleModeSnippet'] } isSingleModeSnippet\n * @property { DynamicSnippetOptionPlugin['isSingleModeSnippetTemplate'] } isSingleModeSnippetTemplate\n * @property { DynamicSnippetOptionPlugin['setOptionsDefaultValues'] } setOptionsDefaultValues\n * @property { DynamicSnippetOptionPlugin['updateTemplate'] } updateTemplate\n * @property { DynamicSnippetOptionPlugin['getModelNameFilter'] } getModelNameFilter\n */\n\n/**\n * @typedef {((arg: {\n *      el: HTMLElement;\n *      template: Template;\n * }) => void)[]} dynamic_snippet_template_updated\n */\n\nexport const DYNAMIC_SNIPPET = SNIPPET_SPECIFIC_END;\n\nclass DynamicSnippetOptionPlugin extends Plugin {\n    static id = \"dynamicSnippetOption\";\n    static shared = [\n        \"fetchDynamicFilters\",\n        \"fetchDynamicSnippetTemplates\",\n        \"getDefaultSnippetFilterId\",\n        \"getDefaultSnippetRecordId\",\n        \"getDefaultSnippetTemplate\",\n        \"getSnippetModelName\",\n        \"getSnippetTitleClasses\",\n        \"getTemplateByKey\",\n        \"isModelSnippetTemplate\",\n        \"isSingleModeSnippet\",\n        \"isSingleModeSnippetTemplate\",\n        \"setOptionsDefaultValues\",\n        \"updateTemplate\",\n        \"getModelNameFilter\",\n    ];\n    modelNameFilter = \"\";\n    fetchedDynamicFilters = [];\n    fetchedDynamicFilterTemplates = [];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(DYNAMIC_SNIPPET, DynamicSnippetOption)],\n        builder_actions: {\n            DynamicFilterAction,\n            DynamicSnippetTemplateAction,\n            DynamicModelAction,\n            DynamicRecordAction,\n            CustomizeTemplateAction,\n            NumberOfRecordsAction,\n        },\n        on_snippet_dropped_handlers: this.onSnippetDropped.bind(this),\n        is_unremovable_selector: \".s_dynamic_snippet_title\",\n    };\n    setup() {\n        this.dynamicFiltersCache = new Cache(this._fetchDynamicFilters, JSON.stringify);\n        this.dynamicFilterTemplatesCache = new Cache(\n            this._fetchDynamicSnippetTemplates,\n            JSON.stringify\n        );\n    }\n    destroy() {\n        super.destroy();\n        this.dynamicFiltersCache.invalidate();\n        this.dynamicFilterTemplatesCache.invalidate();\n    }\n    getModelNameFilter() {\n        return this.modelNameFilter;\n    }\n    async onSnippetDropped({ snippetEl }) {\n        if (snippetEl.matches(DynamicSnippetOption.selector)) {\n            await this.setOptionsDefaultValues(snippetEl, this.modelNameFilter);\n        }\n    }\n    async setOptionsDefaultValues(snippetEl, modelNameFilter, contextualFilterDomain = []) {\n        await this.fetchDynamicFilters({\n            model_name: modelNameFilter,\n            search_domain: contextualFilterDomain,\n        });\n        await this.fetchDynamicSnippetTemplates(modelNameFilter);\n\n        const dynamicFilters = {};\n        for (const dynamicFilter of this.fetchedDynamicFilters) {\n            dynamicFilters[dynamicFilter.id] = dynamicFilter;\n        }\n        const dynamicFilterTemplates = {};\n        for (const dynamicFilterTemplate of this.fetchedDynamicFilterTemplates) {\n            dynamicFilterTemplates[dynamicFilterTemplate.key] = dynamicFilterTemplate;\n        }\n        const defaultModelName = modelNameFilter || this.fetchedDynamicFilters[0]?.model_name;\n        const isSingleMode = this.isSingleModeSnippet({\n            ...snippetEl.dataset,\n            snippetModel: defaultModelName,\n        });\n        // The snippet simply gets its template from a \"template class\"\n        // when provided. Otherwise, it will use a default template.\n        let defaultTemplate = this.fetchedDynamicFilterTemplates.find((template) =>\n            snippetEl.classList.contains(this.getTemplateClass(template.key))\n        );\n        if (!defaultTemplate) {\n            defaultTemplate = this.getDefaultSnippetTemplate(defaultModelName, isSingleMode);\n        }\n        if (isSingleMode) {\n            if (defaultModelName) {\n                setDatasetIfUndefined(snippetEl, \"snippetModel\", defaultModelName);\n            }\n            const defaultSnippetRecordId = await this.getDefaultSnippetRecordId(defaultModelName);\n            if (defaultSnippetRecordId) {\n                setDatasetIfUndefined(snippetEl, \"snippetResId\", defaultSnippetRecordId);\n            }\n            setDatasetIfUndefined(snippetEl, \"templateKey\", defaultTemplate.key);\n            this.updateTemplate(snippetEl, defaultTemplate);\n        } else {\n            let selectedFilterId = snippetEl.dataset[\"filterId\"];\n            if (Object.keys(dynamicFilters).length > 0) {\n                if (!snippetEl.dataset.numberOfRecords) {\n                    snippetEl.dataset[\"numberOfRecords\"] = this.fetchedDynamicFilters[0].limit;\n                }\n                const defaultFilterId = this.fetchedDynamicFilters[0].id;\n                if (!dynamicFilters[selectedFilterId]) {\n                    snippetEl.dataset[\"filterId\"] = defaultFilterId;\n                    selectedFilterId = defaultFilterId;\n                }\n            }\n            if (\n                dynamicFilters[selectedFilterId] &&\n                !dynamicFilterTemplates[snippetEl.dataset[\"templateKey\"]]\n            ) {\n                snippetEl.dataset[\"templateKey\"] = defaultTemplate.key;\n                this.updateTemplate(snippetEl, defaultTemplate);\n            }\n        }\n    }\n    getTemplateByKey(templateKey) {\n        return (\n            templateKey && this.fetchedDynamicFilterTemplates.find(({ key }) => key === templateKey)\n        );\n    }\n    getTemplateClass(templateKey) {\n        return templateKey.replace(/.*\\.dynamic_filter_template_/, \"s_\");\n    }\n    updateTemplate(el, template) {\n        const newTemplateKey = template.key;\n        const oldTemplateKey = el.dataset.templateKey;\n        const oldTemplate = this.getTemplateByKey(oldTemplateKey);\n        el.dataset.templateKey = newTemplateKey;\n        if (oldTemplateKey) {\n            el.classList.remove(this.getTemplateClass(oldTemplateKey));\n        }\n        el.classList.add(this.getTemplateClass(newTemplateKey));\n\n        if (template.numOfEl) {\n            el.dataset.numberOfElements = template.numOfEl;\n        } else {\n            delete el.dataset.numberOfElements;\n        }\n        if (template.numOfElSm) {\n            el.dataset.numberOfElementsSmallDevices = template.numOfElSm;\n        } else {\n            delete el.dataset.numberOfElementsSmallDevices;\n        }\n        if (template.numOfElFetch) {\n            el.dataset.numberOfRecords = template.numOfElFetch;\n        }\n        if (template.extraClasses) {\n            el.dataset.extraClasses = template.extraClasses;\n        } else {\n            delete el.dataset.extraClasses;\n        }\n        if (template.columnClasses) {\n            el.dataset.columnClasses = template.columnClasses;\n        } else {\n            delete el.dataset.columnClasses;\n        }\n        if (oldTemplate) {\n            const snippetContainerEl = el.querySelector(\".s_dynamic_snippet_container\");\n            const snippetContentEl = el.querySelector(\".s_dynamic_snippet_content\");\n            snippetContainerEl.classList.remove(\n                ...(oldTemplate.containerClasses?.split(\" \") || [])\n            );\n            snippetContainerEl.classList.add(\n                ...(template.containerClasses || \"container\").split(\" \")\n            );\n            snippetContentEl.classList.remove(...(oldTemplate.contentClasses?.split(\" \") || []));\n            snippetContentEl.classList.add(...(template.contentClasses?.split(\" \") || []));\n            el.classList.remove(...(oldTemplate.extraSnippetClasses?.split(\" \") || []));\n            el.classList.add(...(template.extraSnippetClasses?.split(\" \") || []));\n        }\n        this.dispatchTo(\"dynamic_snippet_template_updated\", { el: el, template: template });\n    }\n    async fetchDynamicFilters(params) {\n        this.fetchedDynamicFilters = await this.dynamicFiltersCache.read(params);\n        return this.fetchedDynamicFilters;\n    }\n    async _fetchDynamicFilters(params) {\n        return rpc(\"/website/snippet/options_filters\", params);\n    }\n    async fetchDynamicSnippetTemplates(modelName) {\n        this.fetchedDynamicFilterTemplates = await this.dynamicFilterTemplatesCache.read({\n            filter_name: modelName.replaceAll(\".\", \"_\"),\n        });\n        return this.fetchedDynamicFilterTemplates;\n    }\n    async _fetchDynamicSnippetTemplates(params) {\n        return rpc(\"/website/snippet/filter_templates\", params);\n    }\n    isSingleModeSnippet({ numberOfRecords, ...params }) {\n        // TODO: Currently, we need to verify that at least one template is\n        // available for single record mode to be enabled. This check should be\n        // removed once all single record templates have been added.\n        return !!(\n            parseInt(numberOfRecords) === 1 &&\n            this.getDefaultSnippetTemplate(this.getSnippetModelName(params), true) &&\n            !params.carouselInterval\n        );\n    }\n    isSingleModeSnippetTemplate(key) {\n        return key.includes(\"_single_\");\n    }\n    isModelSnippetTemplate(key, modelName) {\n        return key.includes(`_${modelName.replaceAll(\".\", \"_\")}_`);\n    }\n    getDefaultSnippetTemplate(modelName, singleMode) {\n        if (modelName) {\n            // Return the default snippet template associated with the current\n            // model for either single or multi-record modes.\n            return this.fetchedDynamicFilterTemplates.find((template) => {\n                const isSingleTemplate = this.isSingleModeSnippetTemplate(template.key);\n                return (\n                    this.isModelSnippetTemplate(template.key, modelName) &&\n                    (singleMode ? isSingleTemplate : !isSingleTemplate)\n                );\n            });\n        }\n    }\n    async getDefaultSnippetRecordId(modelName) {\n        const defaultRecrod = await this.services.orm.searchRead(\n            modelName,\n            [[\"is_published\", \"=\", true]],\n            [\"id\"],\n            { limit: 1 }\n        );\n        return defaultRecrod[0]?.id || \"\";\n    }\n    getDefaultSnippetFilterId(modelName) {\n        return this.fetchedDynamicFilters.find(({ model_name }) => model_name === modelName).id;\n    }\n    getSnippetModelName(snippetData) {\n        return (\n            snippetData.snippetModel ||\n            this.fetchedDynamicFilters.find(({ id }) => id === parseInt(snippetData.filterId))\n                ?.model_name\n        );\n    }\n    getSnippetTitleClasses(position) {\n        const classes = {\n            left: \"d-flex justify-content-between s_dynamic_snippet_title_aside col-lg-3 flex-lg-column justify-content-lg-start\",\n            top: \"d-flex justify-content-between\",\n            none: \"d-none\",\n        };\n        return position ? classes[position] : classes;\n    }\n}\n\nexport class DynamicFilterAction extends BuilderAction {\n    static id = \"dynamicFilter\";\n    static dependencies = [\"dynamicSnippetOption\"];\n    isApplied({ editingElement: el, params }) {\n        return parseInt(el.dataset.filterId) === params.id;\n    }\n    async apply({ editingElement: el, params }) {\n        const utils = this.dependencies.dynamicSnippetOption;\n        let defaultTemplate = params.defaultTemplate;\n        el.dataset.filterId = params.id;\n        // Only if filter's model name changed\n        if (\n            !el.dataset.templateKey ||\n            !utils.isModelSnippetTemplate(el.dataset.templateKey, params.model_name)\n        ) {\n            if (utils.isSingleModeSnippet(el.dataset)) {\n                el.dataset.snippetModel = params.model_name;\n                delete el.dataset.filterId;\n                defaultTemplate = utils.getDefaultSnippetTemplate(params.model_name, true);\n                el.dataset.snippetResId = await utils.getDefaultSnippetRecordId(params.model_name);\n            }\n            utils.updateTemplate(el, defaultTemplate);\n        }\n    }\n}\nexport class DynamicSnippetTemplateAction extends BuilderAction {\n    static id = \"dynamicSnippetTemplate\";\n    static dependencies = [\"dynamicSnippetOption\"];\n    isApplied({ editingElement: el, params }) {\n        return el.dataset.templateKey === params.key;\n    }\n    apply({ editingElement: el, params }) {\n        this.dependencies.dynamicSnippetOption.updateTemplate(el, params);\n    }\n}\nexport class CustomizeTemplateAction extends BuilderAction {\n    static id = \"customizeTemplate\";\n    isApplied({ editingElement: el, params: { mainParam: customDataKey } }) {\n        const customData = JSON.parse(el.dataset.customTemplateData);\n        return customData[customDataKey];\n    }\n    apply({ editingElement: el, params: { mainParam: customDataKey }, value }) {\n        const customData = JSON.parse(el.dataset.customTemplateData);\n        customData[customDataKey] = true;\n        el.dataset.customTemplateData = JSON.stringify(customData);\n    }\n    clean({ editingElement: el, params: { mainParam: customDataKey }, value }) {\n        const customData = JSON.parse(el.dataset.customTemplateData);\n        customData[customDataKey] = false;\n        el.dataset.customTemplateData = JSON.stringify(customData);\n    }\n}\nexport class DynamicModelAction extends BuilderAction {\n    static id = \"dynamicModel\";\n    static dependencies = [\"dynamicSnippetOption\"];\n    isApplied({ editingElement: el, params }) {\n        return el.dataset.snippetModel === params.mainParam;\n    }\n    async apply({ editingElement: el, params: { mainParam: modelName } }) {\n        const utils = this.dependencies.dynamicSnippetOption;\n        // Update the snippet data attributes (only available in the\n        // \"single record\" mode).\n        if (el.dataset.snippetModel !== modelName) {\n            el.dataset.snippetModel = modelName;\n            el.dataset.snippetResId = await utils.getDefaultSnippetRecordId(modelName);\n            utils.updateTemplate(el, utils.getDefaultSnippetTemplate(modelName, true));\n        }\n    }\n}\nexport class DynamicRecordAction extends BuilderAction {\n    static id = \"dynamicRecord\";\n    getValue({ editingElement }) {\n        const id = editingElement.dataset.snippetResId;\n        if (id) {\n            return JSON.stringify({ id: parseInt(id) });\n        }\n    }\n    apply({ editingElement, value }) {\n        const { id } = JSON.parse(value);\n        editingElement.dataset.snippetResId = id;\n    }\n}\nexport class NumberOfRecordsAction extends BuilderAction {\n    static id = \"numberOfRecords\";\n    static dependencies = [\"dynamicSnippetOption\", \"builderActions\"];\n\n    setup() {\n        this.defaultRecordId = \"\";\n        this.previousTemplate = false;\n        this.utils = this.dependencies.dynamicSnippetOption;\n    }\n    async load({ editingElement }) {\n        this.modelName = this.utils.getSnippetModelName(editingElement.dataset);\n        this.defaultRecordId = await this.utils.getDefaultSnippetRecordId(this.modelName);\n    }\n    isApplied({ editingElement: el, params }) {\n        return el.dataset.numberOfRecords === params.mainParam;\n    }\n    apply({ editingElement: el, params }) {\n        const isSingleModeBefore = this.utils.isSingleModeSnippet(el.dataset);\n        el.dataset.numberOfRecords = params.mainParam;\n        // Changing the number of records should automatically switch to a\n        // \"single record\" filter mode if only one record is selected, and\n        // conversely, revert to the default filter mode when more than one\n        // record is selected.\n        const isSingleModeAfter = this.utils.isSingleModeSnippet(el.dataset);\n        const switchMode = isSingleModeBefore !== isSingleModeAfter;\n        if (switchMode) {\n            const canUsePreviousTemplate =\n                !!this.previousTemplate &&\n                this.utils.isModelSnippetTemplate(this.previousTemplate.key, this.modelName) &&\n                !!this.utils.isSingleModeSnippetTemplate(this.previousTemplate.key) ===\n                    isSingleModeAfter;\n            const newModeDefaultTemplate = !canUsePreviousTemplate\n                ? this.utils.getDefaultSnippetTemplate(this.modelName, isSingleModeAfter)\n                : this.previousTemplate;\n            this.previousTemplate = this.utils.getTemplateByKey(el.dataset.templateKey);\n            if (isSingleModeAfter) {\n                // Remove useless data on the target and set the single\n                // record default values.\n                delete el.dataset.filterId;\n                el.dataset.snippetModel = this.modelName;\n                el.dataset.snippetResId = this.defaultRecordId;\n            } else {\n                el.dataset.filterId = this.utils.getDefaultSnippetFilterId(this.modelName);\n                delete el.dataset.snippetModel;\n                delete el.dataset.snippetResId;\n            }\n            // Update the snippet title section.\n            const titleEl = el.querySelector(\".s_dynamic_snippet_title\");\n            const classAction = this.dependencies.builderActions.getAction(\"classAction\");\n            const titleClasses = Object.values(this.utils.getSnippetTitleClasses()).find(\n                (classes) =>\n                    titleEl.matches(\n                        classes\n                            .split(\" \")\n                            .map((c) => \".\" + c)\n                            .join(\"\")\n                    )\n            );\n            classAction.clean({\n                editingElement: titleEl,\n                params: { mainParam: titleClasses },\n            });\n            classAction.apply({\n                editingElement: titleEl,\n                params: {\n                    mainParam: this.utils.getSnippetTitleClasses(\n                        isSingleModeAfter ? \"none\" : \"top\"\n                    ),\n                },\n            });\n            return this.utils.updateTemplate(el, newModeDefaultTemplate);\n        }\n    }\n}\n\nexport function setDatasetIfUndefined(snippetEl, optionName, value) {\n    if (snippetEl.dataset[optionName] === undefined) {\n        snippetEl.dataset[optionName] = value;\n    }\n}\n\nregistry.category(\"website-plugins\").add(DynamicSnippetOptionPlugin.id, DynamicSnippetOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport {\n    SNIPPET_SPECIFIC_BEFORE,\n    END,\n    VERTICAL_ALIGNMENT,\n} from \"@html_builder/utils/option_sequence\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class EcommCategoriesShowcaseOption extends BaseOptionComponent {\n    static template = \"website.EcommCategoriesShowcaseOption\";\n    static selector = \".s_ecomm_categories_showcase\";\n}\nexport class EcommCategoriesShowcaseBlockDesign extends BaseOptionComponent {\n    static template = \"website.EcommCategoriesShowcaseBlockDesign\";\n    static selector = \".s_ecomm_categories_showcase_block\";\n}\nexport class EcommCategoriesShowcaseBlocksDesign extends BaseOptionComponent {\n    static template = \"website.EcommCategoriesShowcaseBlocksDesign\";\n    static selector = \".s_ecomm_categories_showcase\";\n}\n\nclass EcommCategoriesShowcaseOptionPlugin extends Plugin {\n    static id = \"ecommCategoriesShowcaseOption\";\n\n    static DEFAULT_BLOCK_COUNT = 3;\n    static MIN_BLOCK_COUNT = 2;\n    static MAX_BLOCK_COUNT = 4;\n    static GAP_CLASS = \"gap-4\";\n    static DEFAULT_ROUNDNESS = \"rounded-2\";\n    static NO_ROUNDNESS = \"rounded-0\";\n    static ROUNDNESS_CLASSES = [\n        \"rounded-0\",\n        \"rounded-1\",\n        \"rounded-2\",\n        \"rounded-3\",\n        \"rounded-4\",\n        \"rounded-5\",\n    ];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(SNIPPET_SPECIFIC_BEFORE, EcommCategoriesShowcaseOption),\n            withSequence(VERTICAL_ALIGNMENT, EcommCategoriesShowcaseBlockDesign),\n            withSequence(END, EcommCategoriesShowcaseBlocksDesign),\n        ],\n        builder_actions: {\n            BlockCountAction,\n            SpacingToggleAction,\n        },\n        dropzone_selector: {\n            selector: \".s_ecomm_categories_showcase_block\",\n            dropNear: \".s_ecomm_categories_showcase_block\",\n        },\n    };\n\n    static _updateBlocksRoundness(editingElement, roundnessClass) {\n        const blocks = editingElement.querySelectorAll(\".s_ecomm_categories_showcase_block\");\n        blocks.forEach((block) => {\n            block.classList.remove(...EcommCategoriesShowcaseOptionPlugin.ROUNDNESS_CLASSES);\n            block.classList.add(roundnessClass);\n        });\n    }\n}\n\nclass BlockCountAction extends BuilderAction {\n    static id = \"blockCount\";\n\n    getValue({ editingElement }) {\n        const wrapper = editingElement.querySelector(\".s_ecomm_categories_showcase_wrapper\");\n        if (!wrapper) {\n            return EcommCategoriesShowcaseOptionPlugin.DEFAULT_BLOCK_COUNT.toString();\n        }\n        return wrapper.querySelectorAll(\".s_ecomm_categories_showcase_block\").length.toString();\n    }\n\n    apply({ editingElement, value }) {\n        const wrapper = editingElement.querySelector(\".s_ecomm_categories_showcase_wrapper\");\n        if (!wrapper) {\n            return;\n        }\n\n        const count = parseInt(value, 10);\n        if (\n            isNaN(count) ||\n            count < EcommCategoriesShowcaseOptionPlugin.MIN_BLOCK_COUNT ||\n            count > EcommCategoriesShowcaseOptionPlugin.MAX_BLOCK_COUNT\n        ) {\n            return;\n        }\n\n        let blocks = wrapper.querySelectorAll(\".s_ecomm_categories_showcase_block\");\n\n        // Remove blocks if needed\n        while (blocks.length > count) {\n            const blockToRemove = blocks[blocks.length - 1];\n            blockToRemove.remove();\n            blocks = wrapper.querySelectorAll(\".s_ecomm_categories_showcase_block\");\n        }\n\n        // Add blocks if needed\n        while (blocks.length < count) {\n            const newBlock = blocks[0].cloneNode(true);\n            wrapper.appendChild(newBlock);\n            blocks = wrapper.querySelectorAll(\".s_ecomm_categories_showcase_block\");\n        }\n    }\n\n    isApplied({ editingElement, value }) {\n        const wrapper = editingElement.querySelector(\".s_ecomm_categories_showcase_wrapper\");\n        if (!wrapper) {\n            return false;\n        }\n        const currentCount = wrapper.querySelectorAll(\".s_ecomm_categories_showcase_block\").length;\n        const targetCount = parseInt(value, 10);\n        return !isNaN(targetCount) && currentCount === targetCount;\n    }\n}\n\nclass SpacingToggleAction extends BuilderAction {\n    static id = \"spacingToggle\";\n\n    isApplied({ editingElement }) {\n        const wrapper = editingElement.querySelector(\".s_ecomm_categories_showcase_wrapper\");\n        return wrapper && wrapper.classList.contains(EcommCategoriesShowcaseOptionPlugin.GAP_CLASS);\n    }\n\n    apply({ editingElement }) {\n        const wrapper = editingElement.querySelector(\".s_ecomm_categories_showcase_wrapper\");\n        if (!wrapper) {\n            return;\n        }\n\n        const hasGap = wrapper.classList.contains(EcommCategoriesShowcaseOptionPlugin.GAP_CLASS);\n        wrapper.classList.toggle(EcommCategoriesShowcaseOptionPlugin.GAP_CLASS);\n\n        // Set roundness based on new state\n        const newRoundness = hasGap\n            ? EcommCategoriesShowcaseOptionPlugin.NO_ROUNDNESS\n            : EcommCategoriesShowcaseOptionPlugin.DEFAULT_ROUNDNESS;\n        EcommCategoriesShowcaseOptionPlugin._updateBlocksRoundness(editingElement, newRoundness);\n    }\n\n    clean({ editingElement }) {\n        const wrapper = editingElement.querySelector(\".s_ecomm_categories_showcase_wrapper\");\n        if (wrapper) {\n            wrapper.classList.remove(EcommCategoriesShowcaseOptionPlugin.GAP_CLASS);\n            EcommCategoriesShowcaseOptionPlugin._updateBlocksRoundness(\n                editingElement,\n                EcommCategoriesShowcaseOptionPlugin.NO_ROUNDNESS\n            );\n        }\n    }\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(EcommCategoriesShowcaseOptionPlugin.id, EcommCategoriesShowcaseOptionPlugin);\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { CodeEditor } from \"@web/core/code_editor/code_editor\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { EditHeadBodyDialog } from \"@website/components/edit_head_body_dialog/edit_head_body_dialog\";\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class EmbedCodeOptionDialog extends Component {\n    static template = \"website.EmbedCodeOptionDialog\";\n    static components = { Dialog, CodeEditor };\n    static props = {\n        title: String,\n        value: String,\n        mode: String,\n        confirm: Function,\n        close: Function,\n    };\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.state = useState({ value: this.props.value });\n    }\n    onCodeChange(newValue) {\n        this.state.value = newValue;\n    }\n    onConfirm() {\n        this.props.confirm(this.state.value);\n        this.props.close();\n    }\n    onInjectHeadOrBody() {\n        this.dialog.add(EditHeadBodyDialog);\n        this.props.close();\n    }\n}\n", "import { BEGIN } from \"@html_builder/utils/option_sequence\";\nimport { EmbedCodeOptionDialog } from \"./embed_code_option_dialog\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { cloneContentEls } from \"@website/js/utils\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class EmbedCodeOption extends BaseOptionComponent {\n    static template = \"website.EmbedCodeOption\";\n    static selector = \".s_embed_code\";\n}\n\nclass EmbedCodeOptionPlugin extends Plugin {\n    static id = \"embedCodeOption\";\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(BEGIN, EmbedCodeOption)],\n        so_content_addition_selector: [\".s_embed_code\"],\n        builder_actions: {\n            EditCodeAction,\n        },\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n    };\n\n    cleanForSave({ root }) {\n        // Saving Embed Code snippets with <script> in the database, as these\n        // elements are removed in edit mode.\n        for (const embedCodeEl of root.querySelectorAll(\".s_embed_code\")) {\n            const embedTemplateEl = embedCodeEl.querySelector(\".s_embed_code_saved\");\n            if (embedTemplateEl) {\n                embedCodeEl\n                    .querySelector(\".s_embed_code_embedded\")\n                    .replaceChildren(cloneContentEls(embedTemplateEl.content, true));\n            }\n        }\n    }\n}\n\nexport class EditCodeAction extends BuilderAction {\n    static id = \"editCode\";\n    async load({ editingElement }) {\n        let newContent;\n        await new Promise((resolve) => {\n            this.services.dialog.add(\n                EmbedCodeOptionDialog,\n                {\n                    title: _t(\"Edit embedded code\"),\n                    value: this.getTemplateEl(editingElement).innerHTML.trim(),\n                    mode: \"xml\",\n                    confirm: (newValue) => {\n                        newContent = newValue;\n                    },\n                },\n                { onClose: resolve }\n            );\n        });\n        return newContent;\n    }\n    apply({ editingElement, loadResult: content }) {\n        if (!content) {\n            return;\n        }\n        // Remove scripts tags from the DOM as we don't want them to\n        // interfere during edition, but keeps them in a\n        // `<template>` that will be saved to the database.\n        this.getTemplateEl(editingElement).content.replaceChildren(cloneContentEls(content, true));\n        editingElement\n            .querySelector(\".s_embed_code_embedded\")\n            .replaceChildren(cloneContentEls(content));\n    }\n    getTemplateEl(editingElement) {\n        return editingElement.querySelector(\"template.s_embed_code_saved\");\n    }\n}\n\nregistry.category(\"website-plugins\").add(EmbedCodeOptionPlugin.id, EmbedCodeOptionPlugin);\n", "import { Component, useState } from \"@odoo/owl\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { useBus } from \"@web/core/utils/hooks\";\n\nexport class EmphasizeAnimatedText extends Component {\n    static template = \"website.EmphasizeAnimatedText\";\n    static components = { CheckBox };\n    static props = [];\n\n    setup() {\n        this.state = useState({\n            animatedTextEmphasized: this.isAnimatedTextEmphasized(),\n            hasAnimatedText: this.hasAnimatedText(),\n        });\n        useBus(this.env.editorBus, \"DOM_UPDATED\", (ev) => {\n            this.state.hasAnimatedText = this.hasAnimatedText();\n        });\n    }\n\n    toggleEmphasizeAnimatedText() {\n        this.state.animatedTextEmphasized = this.env.editor.document.body.classList.toggle(\n            \"o_animated_text_emphasized\"\n        );\n    }\n\n    isAnimatedTextEmphasized() {\n        return !!this.env.editor.document.body.classList.contains(\"o_animated_text_emphasized\");\n    }\n\n    hasAnimatedText() {\n        return !!this.env.editor.editable.querySelector(\".o_animated_text\");\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { getCommonAncestor, selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class FacebookOption extends BaseOptionComponent {\n    static template = \"website.FacebookOption\";\n    static selector = \".o_facebook_page\";\n}\n\nclass FacebookOptionPlugin extends Plugin {\n    static id = \"facebookOption\";\n    static dependencies = [\"history\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [FacebookOption],\n        so_content_addition_selector: [\".o_facebook_page\"],\n        builder_actions: {\n            DataAttributeListAction,\n            CheckFacebookLinkAction,\n        },\n        normalize_handlers: this.normalize.bind(this),\n    };\n\n    normalize(root) {\n        for (const element of selectElements(root, \".o_facebook_page\")) {\n            let desiredHeight;\n            if (element.dataset.tabs) {\n                desiredHeight = element.dataset.tabs === \"events\" ? 300 : 500;\n            } else if (element.dataset.small_header) {\n                desiredHeight = 70;\n            } else {\n                desiredHeight = 150;\n            }\n            if (desiredHeight !== element.dataset.height) {\n                element.dataset.height = desiredHeight;\n            }\n        }\n\n        const nodes = [...selectElements(root, \".o_facebook_page:not([data-href])\")];\n        if (nodes.length) {\n            this.loadAndSetEmptyLink(nodes);\n        }\n    }\n\n    async loadAndSetEmptyLink(nodes) {\n        // TODO: look in shared cache with social info: was SocialMediaOption.getDbSocialValuesCache()\n        if (this.facebookUrl) {\n            this.setEmptyLink(nodes);\n            return;\n        }\n        // Fetches the default url for facebook page from website config\n        const res = await this.services.orm.read(\n            \"website\",\n            [this.services.website.currentWebsite.id],\n            [\"social_facebook\"]\n        );\n        if (res) {\n            this.facebookUrl = res[0].social_facebook || \"https://www.facebook.com/Odoo\";\n\n            // WARNING: the call to ignoreDOMMutations is very dangerous,\n            // and should be avoided in most cases (if you think you need those, ask html_editor team)\n            const hasChanged = this.dependencies.history.ignoreDOMMutations(() =>\n                this.setEmptyLink(nodes)\n            );\n\n            if (hasChanged) {\n                const commonAncestor = getCommonAncestor(nodes, this.editable);\n                this.dispatchTo(\"content_manually_updated_handlers\", commonAncestor);\n                this.config.onChange({ isPreviewing: false });\n            }\n        }\n    }\n\n    setEmptyLink(nodes) {\n        let hasChanged = false;\n        for (const element of nodes) {\n            if (!element.dataset.href) {\n                element.dataset.href = this.facebookUrl;\n                hasChanged = true;\n            }\n        }\n        return hasChanged;\n    }\n}\n\nexport class DataAttributeListAction extends BuilderAction {\n    static id = \"dataAttributeList\";\n    isApplied({ editingElement, params: { mainParam } = {}, value }) {\n        return (editingElement.dataset[mainParam]?.split(\",\") || []).includes(value);\n    }\n    apply({ editingElement, params: { mainParam } = {}, value }) {\n        editingElement.dataset[mainParam] = [\n            ...(editingElement.dataset[mainParam]?.split(\",\") || []),\n            value,\n        ].join(\",\");\n    }\n    clean({ editingElement, params: { mainParam } = {}, value }) {\n        editingElement.dataset[mainParam] = (editingElement.dataset[mainParam]?.split(\",\") || [])\n            .filter((e) => e !== value)\n            .join(\",\");\n    }\n}\nexport class CheckFacebookLinkAction extends BuilderAction {\n    static id = \"checkFacebookLink\";\n    setup() {\n        this.closeNotif = () => {};\n    }\n    apply({ editingElement, value }) {\n        editingElement.dataset.id = \"\";\n        const id = this.idFromFacebookLink(value);\n        if (id) {\n            editingElement.dataset.id = id;\n            this.checkFacebookId(id).then((ok) => {\n                this.closeNotif();\n                if (ok) {\n                    this.closeNotif = () => {};\n                } else {\n                    this.closeNotif = this.services.notification.add(\n                        _t(\"We couldn't find the Facebook page\"),\n                        { type: \"warning\" }\n                    );\n                }\n            });\n        } else {\n            this.closeNotif();\n            this.closeNotif = this.services.notification.add(\n                _t(\"You didn't provide a valid Facebook link\"),\n                { type: \"warning\" }\n            );\n        }\n    }\n    idFromFacebookLink(url) {\n        // Patterns matched by the regex (all relate to existing pages,\n        // in spite of the URLs containing \"profile.php\" or \"people\"):\n        // - https://www.facebook.com/<pagewithaname>\n        // - http://www.facebook.com/<page.with.a.name>\n        // - www.facebook.com/<fbid>\n        // - facebook.com/profile.php?id=<fbid>\n        // - www.facebook.com/<name>-<fbid>  - NB: the name doesn't matter\n        // - www.fb.com/people/<name>/<fbid>  - same\n        // - m.facebook.com/p/<name>-<fbid>  - same\n        // The regex is kept as a huge one-liner for performance as it is\n        // compiled once on script load. The only way to split it on several\n        // lines is with the RegExp constructor, which is compiled on runtime.\n        const match = url\n            .trim()\n            .match(\n                /^(https?:\\/\\/)?((www\\.)?(fb|facebook)|(m\\.)?facebook)\\.com\\/(((profile\\.php\\?id=|people\\/([^/?#]+\\/)?|(p\\/)?[^/?#]+-)(?<id>[0-9]{12,16}))|(?<nameid>[\\w.]+))($|[/?# ])/\n            );\n\n        return match?.groups.nameid || match?.groups.id;\n    }\n\n    async checkFacebookId(id) {\n        const res = await fetch(`https://graph.facebook.com/${id}/picture`);\n        return res.ok;\n    }\n}\n\nregistry.category(\"website-plugins\").add(FacebookOptionPlugin.id, FacebookOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { BEGIN } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\n\nexport class FaqHorizontalOption extends BaseOptionComponent {\n    static template = \"website.FaqHorizontalOption\";\n    static selector = \".s_faq_horizontal\";\n}\n\nclass FaqHorizontalOptionPlugin extends Plugin {\n    static id = \"faqHorizontalOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(BEGIN, FaqHorizontalOption)],\n    };\n}\nregistry.category(\"website-plugins\").add(FaqHorizontalOptionPlugin.id, FaqHorizontalOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { onWillStart } from \"@odoo/owl\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nexport class FooterCopyrightOption extends BaseOptionComponent {\n    static template = \"website.FooterCopyrightOption\";\n    static selector = \".o_footer_copyright\";\n    static editableOnly = false;\n    static groups = [\"website.group_website_designer\"];\n\n    setup() {\n        super.setup();\n        this.languages = null;\n\n        onWillStart(async () => {\n            this.languages = await rpc(\"/website/get_languages\", {}, { cache: true });\n        });\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { FooterCopyrightOption } from \"@website/builder/plugins/options/footer_copyright_option\";\n\nclass FooterCopyrightOptionPlugin extends Plugin {\n    static id = \"footerCopyrightOption\";\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [FooterCopyrightOption],\n    };\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(FooterCopyrightOptionPlugin.id, FooterCopyrightOptionPlugin);\n", "import { registry } from \"@web/core/registry\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport {\n    SNIPPET_SPECIFIC_END,\n    SNIPPET_SPECIFIC_NEXT,\n    splitBetween,\n} from \"@html_builder/utils/option_sequence\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { FooterTemplateChoice, FooterTemplateOption } from \"./footer_template_option\";\nimport { reactive } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { BorderConfigurator } from \"@html_builder/plugins/border_configurator_option\";\nimport { ShadowOption } from \"@html_builder/plugins/shadow_option\";\n\n/** @typedef {import(\"@odoo/owl\").Component} Component */\n\n/**\n * @typedef { Object } FooterOptionShared\n * @property { FooterOptionPlugin['getFooterTemplates'] } getFooterTemplates\n */\n/**\n * @typedef {(() => Promise<{\n *     key: string,\n *     Component: Component,\n *     props: any,\n * }[]>)[]} footer_templates_providers\n */\n\nconst [\n    FOOTER_TEMPLATE,\n    FOOTER_COLORS,\n    FOOTER_WIDTH,\n    FOOTER_SLIDEOUT,\n    FOOTER_SCROLL_TO,\n    FOOTER_COPYRIGHT,\n    FOOTER_BORDER,\n    ...__ERROR_CHECK__\n] = splitBetween(SNIPPET_SPECIFIC_NEXT, SNIPPET_SPECIFIC_END, 7);\nif (__ERROR_CHECK__.length > 0) {\n    console.error(\"Wrong count in footer option split\");\n}\n\nexport {\n    FOOTER_TEMPLATE,\n    FOOTER_COLORS,\n    FOOTER_WIDTH,\n    FOOTER_SLIDEOUT,\n    FOOTER_SCROLL_TO,\n    FOOTER_COPYRIGHT,\n    FOOTER_BORDER,\n};\n\nexport class FooterWidthOption extends BaseOptionComponent {\n    static template = \"website.FooterWidthOption\";\n    static selector = \"#wrapwrap > footer\";\n    static applyTo =\n        \":is(:scope > #footer > section, .o_footer_copyright) > :is(.container, .container-fluid, .o_container_small)\";\n    static editableOnly = false;\n    static groups = [\"website.group_website_designer\"];\n}\n\nexport class FooterColorsOption extends BaseOptionComponent {\n    static template = \"website.FooterColorsOption\";\n    static selector = \"#wrapwrap > footer\";\n    static editableOnly = false;\n    static groups = [\"website.group_website_designer\"];\n}\n\nexport class FooterSlideoutOption extends BaseOptionComponent {\n    static template = \"website.FooterSlideoutOption\";\n    static selector = \"#wrapwrap > footer\";\n    static editableOnly = false;\n    static groups = [\"website.group_website_designer\"];\n}\n\nexport class ToggleFooterCopyrightOption extends BaseOptionComponent {\n    static template = \"website.ToggleFooterCopyrightOption\";\n    static selector = \"#wrapwrap > footer\";\n    static editableOnly = false;\n    static groups = [\"website.group_website_designer\"];\n}\n\nexport class FooterBorder extends BaseOptionComponent {\n    static template = \"website.FooterBorder\";\n    static selector = \"#wrapwrap > footer\";\n    static applyTo = \"#footer\";\n    static editableOnly = false;\n    static groups = [\"website.group_website_designer\"];\n    static components = { BorderConfigurator, ShadowOption };\n}\n\nexport class FooterScrollToTopOption extends BaseOptionComponent {\n    static template = \"website.FooterScrollToTopOption\";\n    static selector = \"#wrapwrap > footer\";\n    static editableOnly = false;\n    static groups = [\"website.group_website_designer\"];\n}\n\nclass FooterOptionPlugin extends Plugin {\n    static id = \"footerOption\";\n    static dependencies = [\"customizeWebsite\", \"builderActions\"];\n    static shared = [\"getFooterTemplates\"];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(FOOTER_TEMPLATE, FooterTemplateOption),\n            withSequence(FOOTER_WIDTH, FooterWidthOption),\n            withSequence(FOOTER_COLORS, FooterColorsOption),\n            withSequence(FOOTER_SLIDEOUT, FooterSlideoutOption),\n            withSequence(FOOTER_COPYRIGHT, ToggleFooterCopyrightOption),\n            withSequence(FOOTER_BORDER, FooterBorder),\n            withSequence(FOOTER_SCROLL_TO, FooterScrollToTopOption),\n        ],\n        builder_actions: {\n            WebsiteConfigFooterAction,\n        },\n        on_prepare_drag_handlers: this.prepareDrag.bind(this),\n        unremovable_node_predicates: (node) => node.id === \"o_footer_scrolltop\",\n        footer_templates_providers: [\n            () =>\n                [\n                    { name: \"default\", title: _t(\"Default\"), view: \"website.footer_custom\" },\n                    { name: \"descriptive\", title: _t(\"Descriptive\") },\n                    { name: \"centered\", title: _t(\"Centered\") },\n                    { name: \"links\", title: _t(\"Links\") },\n                    { name: \"minimalist\", title: _t(\"Minimalist\") },\n                    { name: \"contact\", title: _t(\"Contact\") },\n                    { name: \"call_to_action\", title: _t(\"Call-to-action\") },\n                    { name: \"headline\", title: _t(\"Headline\") },\n                    { name: \"mega\", title: _t(\"Mega\") },\n                    { name: \"mega_columns\", title: _t(\"Mega Columns\") },\n                    { name: \"mega_links\", title: _t(\"Mega Links\") },\n                    { name: \"mega_cards\", title: _t(\"Mega Cards\") },\n                ].map((info) => ({\n                    key: info.name,\n                    Component: FooterTemplateChoice,\n                    props: {\n                        imgSrc: `/website/static/src/img/snippets_options/footer_template_${info.name}.svg`,\n                        varName: info.name,\n                        view: info.view ?? `website.template_footer_${info.name}`,\n                        title: info.title,\n                    },\n                })),\n        ],\n    };\n\n    prepareDrag() {\n        // Remove the footer scroll effect if it has one (because the footer\n        // dropzone flickers otherwise when it is in grid mode).\n        let restore = () => {};\n        const wrapwrapEl = this.editable;\n        const hasFooterScrollEffect = wrapwrapEl.classList.contains(\"o_footer_effect_enable\");\n        if (hasFooterScrollEffect) {\n            wrapwrapEl.classList.remove(\"o_footer_effect_enable\");\n            restore = () => {\n                wrapwrapEl.classList.add(\"o_footer_effect_enable\");\n            };\n        }\n        return restore;\n    }\n\n    getFooterTemplates() {\n        const templates = reactive([]);\n\n        // we don't wait for all promises to resolve and show the ones available\n        // as soon as they are (and keep them in the order of the providers)\n        const templatesByProvider = this.getResource(\"footer_templates_providers\").map((p) => {\n            const provided = [];\n            Promise.resolve(p()).then((t) => {\n                provided.push(...t);\n                templates.splice(0, Infinity, ...templatesByProvider.flat());\n            });\n            return provided;\n        });\n\n        return templates;\n    }\n}\n\nexport class WebsiteConfigFooterAction extends BuilderAction {\n    static id = \"websiteConfigFooter\";\n    static dependencies = [\"builderActions\", \"customizeWebsite\"];\n    setup() {\n        this.reload = {};\n    }\n    isApplied({ params: { vars } }) {\n        for (const [name, value] of Object.entries(vars)) {\n            if (\n                !this.dependencies.builderActions\n                    .getAction(\"customizeWebsiteVariable\")\n                    .isApplied({ params: { mainParam: name }, value })\n            ) {\n                return false;\n            }\n        }\n        return true;\n    }\n    async apply({ params: { vars, view }, selectableContext }) {\n        const possibleValues = new Set();\n        for (const item of selectableContext.items) {\n            for (const a of item.getActions()) {\n                if (a.actionId === \"websiteConfigFooter\") {\n                    possibleValues.add(a.actionParam.view);\n                }\n            }\n        }\n        await Promise.all([\n            this.dependencies.customizeWebsite.makeSCSSCusto(\n                \"/website/static/src/scss/options/user_values.scss\",\n                vars\n            ),\n            rpc(\"/website/update_footer_template\", {\n                template_key: view,\n                possible_values: [...possibleValues],\n            }),\n        ]);\n    }\n}\n\nregistry.category(\"website-plugins\").add(FooterOptionPlugin.id, FooterOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { useState } from \"@odoo/owl\";\n\nexport class FooterTemplateOption extends BaseOptionComponent {\n    static template = \"website.FooterTemplateOption\";\n    static dependencies = [\"footerOption\"];\n    static selector = \"#wrapwrap > footer\";\n    static editableOnly = false;\n    static groups = [\"website.group_website_designer\"];\n\n    setup() {\n        super.setup();\n        this.footerTemplates = useState(this.dependencies.footerOption.getFooterTemplates());\n    }\n}\n\nexport class FooterTemplateChoice extends BaseOptionComponent {\n    static template = \"website.FooterTemplateChoice\";\n    static props = { title: String, view: String, varName: String, imgSrc: String };\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { SNIPPET_SPECIFIC } from \"@html_builder/utils/option_sequence\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\n/**\n * @typedef {((\n *      activeItemEl: HTMLElement,\n *      optionName: string\n * ) => HTMLElement[])[]} get_gallery_items_handlers\n * @typedef {((\n *      activeItemEl: HTMLElement,\n *      itemEls: HTMLElement[],\n *      optionName: string\n * ) => void)[]} reorder_items_handlers\n */\n\nexport class GalleryElementOption extends BaseOptionComponent {\n    static template = \"website.GalleryElementOption\";\n    static selector =\n        \".s_image_gallery img, .s_carousel .carousel-item, .s_quotes_carousel .carousel-item, .s_carousel_intro .carousel-item, .s_carousel_cards .carousel-item\";\n}\n\nexport class GalleryElementOptionPlugin extends Plugin {\n    static id = \"galleryElementOption\";\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(SNIPPET_SPECIFIC, GalleryElementOption)],\n        builder_actions: {\n            SetGalleryElementPositionAction,\n        },\n    };\n}\n\nexport class SetGalleryElementPositionAction extends BuilderAction {\n    static id = \"setGalleryElementPosition\";\n    apply({ editingElement: activeItemEl, value: position }) {\n        const optionName = activeItemEl.classList.contains(\"carousel-item\")\n            ? \"Carousel\"\n            : \"GalleryImageList\";\n\n        // Get the items to reorder.\n        const itemEls = [];\n        for (const getGalleryItems of this.getResource(\"get_gallery_items_handlers\")) {\n            itemEls.push(...getGalleryItems(activeItemEl, optionName));\n        }\n\n        // Reorder the items.\n        const oldPosition = itemEls.indexOf(activeItemEl);\n        if (oldPosition === 0 && position === \"prev\") {\n            position = \"last\";\n        } else if (oldPosition === itemEls.length - 1 && position === \"next\") {\n            position = \"first\";\n        }\n        itemEls.splice(oldPosition, 1);\n        switch (position) {\n            case \"first\":\n                itemEls.unshift(activeItemEl);\n                break;\n            case \"prev\":\n                itemEls.splice(Math.max(oldPosition - 1, 0), 0, activeItemEl);\n                break;\n            case \"next\":\n                itemEls.splice(oldPosition + 1, 0, activeItemEl);\n                break;\n            case \"last\":\n                itemEls.push(activeItemEl);\n                break;\n        }\n\n        // Update the DOM with the new items order.\n        this.dispatchTo(\"reorder_items_handlers\", activeItemEl, itemEls, optionName);\n    }\n}\n\nregistry.category(\"website-plugins\").add(GalleryElementOptionPlugin.id, GalleryElementOptionPlugin);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\nimport { Component, useState, useRef } from \"@odoo/owl\";\n\n/**\n * @typedef {import('./google_map_option_plugin.js').ApiKeyValidation} ApiKeyValidation\n */\n\nexport class GoogleMapsApiKeyDialog extends Component {\n    static template = \"website.GoogleMapsApiKeyDialog\";\n    static components = { Dialog };\n    static props = {\n        originalApiKey: String,\n        onSave: Function,\n        close: Function,\n    };\n\n    setup() {\n        this.modalRef = useChildRef();\n        /** @type {{ apiKey?: string, apiKeyValidation: ApiKeyValidation }} */\n        this.state = useState({\n            apiKey: this.props.originalApiKey,\n            apiKeyValidation: { isValid: false },\n        });\n        this.apiKeyInput = useRef(\"apiKeyInput\");\n        // @TODO mysterious-egg: the `google_map service` is a duplicate of the\n        // `website_map_service`, but without the dependency on public\n        // interactions. These are used only to restart the interactions once\n        // the API is loaded. We do this in the plugin instead. Once\n        // `html_builder` replaces `website`, we should be able to remove\n        // `website_map_service` since only google_map service will be used.\n        this.googleMapsService = useService(\"google_maps\");\n    }\n\n    async onClickSave() {\n        if (this.state.apiKey) {\n            /** @type {NodeList} */\n            const buttons = this.modalRef.el.querySelectorAll(\"button\");\n            buttons.forEach((button) => button.setAttribute(\"disabled\", true));\n            /** @type {ApiKeyValidation} */\n            const apiKeyValidation = await this.googleMapsService.validateGMapsApiKey(\n                this.state.apiKey\n            );\n            this.state.apiKeyValidation = apiKeyValidation;\n            if (apiKeyValidation.isValid) {\n                await this.props.onSave(this.state.apiKey);\n                this.props.close();\n            }\n            buttons.forEach((button) => button.removeAttribute(\"disabled\"));\n        } else {\n            this.state.apiKeyValidation = {\n                isValid: false,\n                message: _t(\"Enter an API Key\"),\n            };\n        }\n    }\n}\n", "import { useRef, onMounted, useState, useEffect, onWillDestroy } from \"@odoo/owl\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\n/** @import { Coordinates, Place } from './google_maps_option_plugin.js' */\n\nexport class GoogleMapsOption extends BaseOptionComponent {\n    static template = \"website.GoogleMapsOption\";\n    static dependencies = [\"googleMapsOption\"];\n    static selector = \".s_google_map\";\n\n    async setup() {\n        super.setup();\n\n        this.getMapsAPI = this.dependencies.googleMapsOption.getMapsAPI;\n        /** @type {function(Element, Coordinates):Promise<Place | undefined>} */\n        this.getPlace = this.dependencies.googleMapsOption.getPlace;\n        /** @type {function(Element, Place):void} */\n        this.commitPlace = this.dependencies.googleMapsOption.commitPlace;\n\n        this.inputRef = useRef(\"inputRef\");\n        /** @type {{ formattedAddress: string }} */\n        this.state = useState({\n            formattedAddress: this.env.getEditingElement().dataset.pinAddress || \"\",\n        });\n        useEffect(\n            () => {\n                this.env.getEditingElement().dataset.pinAddress = this.state.formattedAddress;\n            },\n            () => [this.state.formattedAddress]\n        );\n        onMounted(async () => {\n            this.initializeAutocomplete(this.inputRef.el);\n        });\n        onWillDestroy(() => {\n            if (this.autocompleteListener) {\n                this.getMapsAPI().event.removeListener(this.autocompleteListener);\n            }\n            // Without this, the Google library injects elements inside the\n            // DOM but does not remove them once the option is closed.\n            for (const container of document.body.querySelectorAll(\".pac-container\")) {\n                container.remove();\n            }\n        });\n    }\n\n    /**\n     * Initialize Google Places API's autocompletion on the option's input.\n     *\n     * @param {Element} inputEl\n     */\n    initializeAutocomplete(inputEl) {\n        if (!this.googleMapsAutocomplete && this.getMapsAPI()) {\n            const mapsAPI = this.getMapsAPI();\n            this.googleMapsAutocomplete = new mapsAPI.places.Autocomplete(inputEl, {\n                types: [\"geocode\"],\n            });\n            this.autocompleteListener = mapsAPI.event.addListener(\n                this.googleMapsAutocomplete,\n                \"place_changed\",\n                this.onPlaceChanged.bind(this)\n            );\n            if (!this.state.formattedAddress) {\n                const editingElement = this.env.getEditingElement();\n                /** @type {Coordinates} */\n                const coordinates = editingElement.dataset.mapGps;\n                this.getPlace(editingElement, coordinates).then((place) => {\n                    if (place?.formatted_address) {\n                        this.state.formattedAddress = place.formatted_address;\n                    }\n                });\n            }\n        }\n    }\n\n    /**\n     * Retrieve the new place given by Google Places API's autocompletion\n     * whenever it sends a signal that the place changed, and send it to the\n     * plugin.\n     */\n    onPlaceChanged() {\n        /** @type {Place | undefined} */\n        const place = this.googleMapsAutocomplete.getPlace();\n        this.onPlaceChanged(this.env.getEditingElement(), place);\n        this.state.formattedAddress = place?.formatted_address || \"\";\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { GoogleMapsApiKeyDialog } from \"./google_maps_api_key_dialog\";\nimport { GoogleMapsOption } from \"./google_maps_option\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\n\n/**\n * A `google.maps.places.PlaceResult` object.\n * Here listed are only the few properties used here. For a full list, see:\n * {@link https://developers.google.com/maps/documentation/javascript/reference/places-service#PlaceResult}\n *\n * @typedef {Object} Place\n * @property {string} [formatted_address]\n * @property {Object} [geometry]\n * @property {Object} [geometry.location]\n * @property {function():number} geometry.location.lat\n * @property {function():number} geometry.location.lng\n */\n/**\n * A string defining GPS coordinates in the form \"`Latitude`,`Longitude`\".\n * @typedef {`${number},${number}`} Coordinates\n */\n/**\n * @typedef {{ isValid: boolean, message?: string }} ApiKeyValidation\n */\n\n/**\n * @typedef { Object } GoogleMapsOptionShared\n * @property { GoogleMapsOptionPlugin['configureGMapsAPI'] } configureGMapsAPI\n * @property { GoogleMapsOptionPlugin['initializeGoogleMaps'] } initializeGoogleMaps\n * @property { GoogleMapsOptionPlugin['failedToInitializeGoogleMaps'] } failedToInitializeGoogleMaps\n * @property { GoogleMapsOptionPlugin['shouldRefetchApiKey'] } shouldRefetchApiKey\n * @property { GoogleMapsOptionPlugin['shouldNotRefetchApiKey'] } shouldNotRefetchApiKey\n * @property { GoogleMapsOptionPlugin['commitPlace'] } commitPlace\n * @property { GoogleMapsOptionPlugin['getPlace'] } getPlace\n * @property { GoogleMapsOptionPlugin['getMapsAPI'] } getMapsAPI\n */\n\nexport class GoogleMapsOptionPlugin extends Plugin {\n    static id = \"googleMapsOption\";\n    static dependencies = [\"history\", \"edit_interaction\"];\n    static shared = [\n        \"configureGMapsAPI\",\n        \"initializeGoogleMaps\",\n        \"failedToInitializeGoogleMaps\",\n        \"shouldRefetchApiKey\",\n        \"shouldNotRefetchApiKey\",\n        \"commitPlace\",\n        \"getPlace\",\n        \"getMapsAPI\",\n    ];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [GoogleMapsOption],\n        so_content_addition_selector: [\".s_google_map\"],\n        on_snippet_dropped_handlers: this.onSnippetDropped.bind(this),\n        builder_actions: {\n            ResetMapColorAction,\n            ShowDescriptionAction,\n        },\n        // TODO remove when the snippet will have a \"Height\" option.\n        keep_overlay_options: (el) => el.matches(\".s_google_map\"),\n    };\n\n    setup() {\n        this.websiteService = this.services.website;\n        this.dialog = this.services.dialog;\n        this.orm = this.services.orm;\n        this.notification = this.services.notification;\n\n        /** @type {Map<Coordinates, Place>} */\n        this.gpsMapCache = new Map();\n\n        /** @type {Map<HTMLElement, Deferred} */\n        this.recentlyDroppedSnippetDeferredInit = new Map();\n    }\n\n    async onSnippetDropped({ snippetEl }) {\n        if (snippetEl.matches(\".s_google_map\")) {\n            const deferredInit = new Deferred();\n            this.recentlyDroppedSnippetDeferredInit.set(snippetEl, deferredInit);\n            this.dependencies.edit_interaction.restartInteractions(snippetEl);\n            const initSuccess = await deferredInit;\n            this.recentlyDroppedSnippetDeferredInit.delete(snippetEl);\n            if (!initSuccess) {\n                return true; // cancel\n            }\n        }\n    }\n\n    failedToInitializeGoogleMaps(editingElement) {\n        this.recentlyDroppedSnippetDeferredInit.get(editingElement)?.resolve(false);\n    }\n\n    getMapsAPI() {\n        return this.mapsAPI;\n    }\n\n    async initializeGoogleMaps(editingElement, mapsAPI) {\n        this.recentlyDroppedSnippetDeferredInit.get(editingElement)?.resolve(true);\n        if (mapsAPI) {\n            this.mapsAPI = mapsAPI;\n            this.placesAPI = mapsAPI.places;\n        }\n        // Try to fail early if there is a configuration issue.\n        return (\n            !!this.placesAPI &&\n            !!(await this.getPlace(editingElement, editingElement.dataset.mapGps))\n        );\n    }\n\n    /**\n     * Take a set of coordinates and perform a search on them to return a\n     * place's formatted address. If it failed, there must be an issue with the\n     * API so remove the snippet.\n     *\n     * @param {Element} editingElement\n     * @param {Coordinates} coordinates\n     * @returns {Promise<Place | undefined>}\n     */\n    async getPlace(editingElement, coordinates) {\n        const place = await this.nearbySearch(coordinates);\n        if (place?.error && !this.isGoogleMapsErrorBeingHandled) {\n            this.notifyGMapsError(editingElement);\n        } else if (!place && !this.isGoogleMapsErrorBeingHandled) {\n            // Somehow the search failed but Google didn't trigger an error.\n            this.undoInitialize?.();\n        } else {\n            return place;\n        }\n    }\n\n    /**\n     * Commit a place's coordinates and address to the cache and to the editing\n     * element's dataset, then re-render the map to reflect it.\n     *\n     * @param {Element} editingElement\n     * @param {Place} place\n     */\n    commitPlace(editingElement, place) {\n        if (place?.geometry) {\n            const location = place.geometry.location;\n            /** @type {Coordinates} */\n            const coordinates = `(${location.lat()},${location.lng()})`;\n            this.gpsMapCache.set(coordinates, place);\n            /** @type {{mapGps: Coordinates, pinAddress: string}} */\n            const currentMapData = editingElement.dataset;\n            const { mapGps, pinAddress } = currentMapData;\n            if (mapGps !== coordinates || pinAddress !== place.formatted_address) {\n                editingElement.dataset.mapGps = coordinates;\n                editingElement.dataset.pinAddress = place.formatted_address;\n                // Restart interactions to re-render the map.\n                this.dispatchTo(\"content_manually_updated_handlers\", editingElement);\n                this.dependencies.history.addStep();\n            }\n        }\n    }\n\n    /**\n     * Test the validity of the API key provided if any. If none was provided,\n     * or the key was invalid, or the `force` argument is `true`, open the API\n     * key dialog to prompt the user to provide a new API key.\n     *\n     * @param {Object} param\n     * @param {string} [param.apiKey]\n     * @returns {Promise<boolean>} true if a new API key was written to db.\n     */\n    async configureGMapsAPI(apiKey) {\n        this.undoInitialize = this.dependencies.history.makeSavePoint();\n        /** @type {number} */\n        const websiteId = this.websiteService.currentWebsite.id;\n\n        /** @type {boolean} */\n        const didReconfigure = await new Promise((resolve) => {\n            let isInvalidated = false;\n            // Open the Google API Key Dialog.\n            this.dialog.add(\n                GoogleMapsApiKeyDialog,\n                {\n                    originalApiKey: apiKey,\n                    onSave: async (newApiKey) => {\n                        await this.orm.write(\"website\", [websiteId], {\n                            google_maps_api_key: newApiKey,\n                        });\n                        this.shouldRefetchApiKey = false;\n                        isInvalidated = true;\n                    },\n                },\n                {\n                    onClose: () => resolve(isInvalidated),\n                }\n            );\n        });\n        return didReconfigure;\n    }\n\n    /**\n     * @param {Coordinates} coordinates\n     * @returns {Promise<Place|{ error: string }|undefined>}\n     */\n    async nearbySearch(coordinates) {\n        const place = this.gpsMapCache.get(coordinates);\n        if (place) {\n            return place;\n        }\n\n        const p = coordinates.substring(1).slice(0, -1).split(\",\");\n        const location = new this.mapsAPI.LatLng(p[0] || 0, p[1] || 0);\n        return new Promise((resolve) => {\n            const placesService = new this.placesAPI.PlacesService(document.createElement(\"div\"));\n            placesService.nearbySearch(\n                {\n                    // Do a 'nearbySearch' followed by 'getDetails' to avoid using\n                    // GMaps Geocoder which the user may not have enabled... but\n                    // ideally Geocoder should be used to get the exact location at\n                    // those coordinates and to limit billing query count.\n                    location,\n                    radius: 1,\n                },\n                (results, status) => {\n                    const GMAPS_CRITICAL_ERRORS = [\n                        this.placesAPI.PlacesServiceStatus.REQUEST_DENIED,\n                        this.placesAPI.PlacesServiceStatus.UNKNOWN_ERROR,\n                    ];\n                    if (status === this.placesAPI.PlacesServiceStatus.OK) {\n                        placesService.getDetails(\n                            {\n                                placeId: results[0].place_id,\n                                fields: [\"geometry\", \"formatted_address\"],\n                            },\n                            (place, status) => {\n                                if (status === this.placesAPI.PlacesServiceStatus.OK) {\n                                    this.gpsMapCache.set(coordinates, place);\n                                    resolve(place);\n                                } else if (GMAPS_CRITICAL_ERRORS.includes(status)) {\n                                    resolve({ error: status });\n                                } else {\n                                    resolve();\n                                }\n                            }\n                        );\n                    } else if (GMAPS_CRITICAL_ERRORS.includes(status)) {\n                        resolve({ error: status });\n                    } else {\n                        resolve();\n                    }\n                }\n            );\n        });\n    }\n\n    /**\n     * Indicates to the user there is an error with the google map API and\n     * re-opens the configuration dialog. For good measure, this also removes\n     * the related snippet entirely as this is what is done in case of critical\n     * error.\n     */\n    notifyGMapsError(editingElement) {\n        // TODO this should be better to detect all errors. This is random.\n        // When misconfigured (wrong APIs enabled), sometimes Google throws\n        // errors immediately (which then reaches this code), sometimes it\n        // throws them later (which then induces an error log in the console\n        // and random behaviors).\n        if (!this.isGoogleMapsErrorBeingHandled) {\n            this.isGoogleMapsErrorBeingHandled = true;\n\n            this.notification.add(\n                _t(\n                    \"A Google Maps error occurred. Make sure to read the key configuration popup carefully.\"\n                ),\n                { type: \"danger\", sticky: true }\n            );\n            // Try again: invalidate the API key then restart interactions.\n            this.orm\n                .write(\"website\", [this.websiteService.currentWebsite.id], {\n                    google_maps_api_key: \"\",\n                })\n                .then(() => {\n                    this.wasApiKeyInvalidated = true;\n                    this.isGoogleMapsErrorBeingHandled = false;\n                    this.dependencies.edit_interaction.restartInteractions(editingElement);\n                });\n        }\n    }\n    shouldRefetchApiKey() {\n        return this.wasApiKeyInvalidated || false;\n    }\n    shouldNotRefetchApiKey() {\n        this.wasApiKeyInvalidated = false;\n    }\n}\n\nexport class ResetMapColorAction extends BuilderAction {\n    static id = \"resetMapColor\";\n    apply({ editingElement }) {\n        editingElement.dataset.mapColor = \"\";\n    }\n}\nexport class ShowDescriptionAction extends BuilderAction {\n    static id = \"showDescription\";\n    isApplied({ editingElement }) {\n        return !!editingElement.querySelector(\".description\");\n    }\n    apply({ editingElement }) {\n        editingElement.append(renderToElement(\"html_builder.GoogleMapsDescription\"));\n    }\n    clean({ editingElement }) {\n        editingElement.querySelector(\".description\").remove();\n    }\n}\n\nregistry.category(\"website-plugins\").add(GoogleMapsOptionPlugin.id, GoogleMapsOptionPlugin);\n", "/* eslint-disable no-async-promise-executor */\n\nimport { loadJS } from \"@web/core/assets\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { markup } from \"@odoo/owl\";\n\nregistry.category(\"services\").add(\"google_maps\", {\n    dependencies: [\"notification\"],\n    start(env, deps) {\n        const notification = deps[\"notification\"];\n        let gMapsAPIKeyProm;\n        let gMapsAPILoading;\n        const promiseKeys = {};\n        const promiseKeysResolves = {};\n        let lastKey;\n        window.odoo_gmaps_api_post_load = async function odoo_gmaps_api_post_load() {\n            promiseKeysResolves[lastKey]?.();\n        }.bind(this);\n        return {\n            /**\n             * @param {boolean} [refetch=false]\n             */\n            async getGMapsAPIKey(refetch) {\n                if (refetch || !gMapsAPIKeyProm) {\n                    gMapsAPIKeyProm = new Promise(async (resolve) => {\n                        const data = await rpc(\"/website/google_maps_api_key\");\n                        resolve(JSON.parse(data).google_maps_api_key || \"\");\n                    });\n                }\n                return gMapsAPIKeyProm;\n            },\n            /**\n             * @param {boolean} [editableMode=false]\n             * @param {boolean} [refetch=false]\n             */\n            async loadGMapsAPI(editableMode, refetch) {\n                // Note: only need refetch to reload a configured key and load\n                // the library. If the library was loaded with a correct key and\n                // that the key changes meanwhile... it will not work but we can\n                // agree the user can bother to reload the page at that moment.\n                if (refetch || !gMapsAPILoading) {\n                    gMapsAPILoading = new Promise(async (resolve) => {\n                        const key = await this.getGMapsAPIKey(refetch);\n                        lastKey = key;\n\n                        if (key) {\n                            if (!promiseKeys[key]) {\n                                promiseKeys[key] = new Promise((resolve) => {\n                                    promiseKeysResolves[key] = resolve;\n                                });\n                                await loadJS(\n                                    `https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=places&callback=odoo_gmaps_api_post_load&key=${encodeURIComponent(\n                                        key\n                                    )}`\n                                );\n                            }\n                            await promiseKeys[key];\n                            resolve(key);\n                        } else {\n                            if (!editableMode && user.isAdmin) {\n                                const message = _t(\"Cannot load google map.\");\n                                const urlTitle = _t(\"Check your configuration.\");\n                                notification.add(\n                                    markup`<div>\n                                        <span>${message}</span><br/>\n                                        <a href=\"/odoo/action-website.action_website_configuration\">${urlTitle}</a>\n                                    </div>`,\n                                    { type: \"warning\", sticky: true }\n                                );\n                            }\n                            resolve(false);\n                        }\n                    });\n                }\n                return gMapsAPILoading;\n            },\n            /**\n             * Send a request to the Google Maps API to test the validity of the given\n             * API key. Return an object with the error message if any, and a boolean\n             * that is true if the response from the API had a status of 200.\n             *\n             * Note: The response will be 200 so long as the API key has billing, Static\n             * API and Javascript API enabled. However, for our purposes, we also need\n             * the Places API enabled. To deal with that case, we perform a nearby\n             * search immediately after validation. If it fails, the error is handled\n             * and the dialog is re-opened.\n             * @see nearbySearch\n             * @see notifyGMapsError\n             *\n             * @param {string} key\n             * @returns {Promise<ApiKeyValidation>}\n             */\n            async validateGMapsApiKey(key) {\n                if (key) {\n                    try {\n                        const response = await this.fetchGoogleMaps(key);\n                        const isValid = response.status === 200;\n                        return {\n                            isValid,\n                            message: isValid\n                                ? undefined\n                                : _t(\n                                      \"Invalid API Key. The following error was returned by Google: %(error)s\",\n                                      { error: await response.text() }\n                                  ),\n                        };\n                    } catch {\n                        return {\n                            isValid: false,\n                            message: _t(\"Check your connection and try again\"),\n                        };\n                    }\n                } else {\n                    return { isValid: false };\n                }\n            },\n            /**\n             * Send a request to the Google Maps API, using the given API key, so as to\n             * get a response which can be used to test the validity of said key.\n             * This method is set apart so it can be overridden for testing.\n             *\n             * @param {string} key\n             * @returns {Promise<{ status: number }>}\n             */\n            async fetchGoogleMaps(key) {\n                return await fetch(\n                    `https://maps.googleapis.com/maps/api/staticmap?center=belgium&size=10x10&key=${encodeURIComponent(\n                        key\n                    )}`\n                );\n            },\n        };\n    },\n});\n", "export const basicHeaderOptionSettings = {\n    editableOnly: false,\n    selector: \"#wrapwrap > header\",\n    groups: [\"website.group_website_designer\"],\n};\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { BorderConfigurator } from \"@html_builder/plugins/border_configurator_option\";\nimport { ShadowOption } from \"@html_builder/plugins/shadow_option\";\nimport { basicHeaderOptionSettings } from \"./basicHeaderOptionSettings\";\n\nexport class HeaderBoxOption extends BaseOptionComponent {\n    static template = \"website.HeaderBoxOption\";\n    static applyTo = \".navbar:not(.d-none)\";\n\n    static components = { BorderConfigurator, ShadowOption };\n\n    setup() {\n        super.setup();\n        this.domState = useDomState((editingElement) => ({\n            withRoundCorner: !editingElement.classList.contains(\"o_header_force_no_radius\"),\n        }));\n    }\n}\n\nObject.assign(HeaderBoxOption, basicHeaderOptionSettings);\n", "import {\n    getCurrentShadow,\n    getDefaultShadow,\n    SetShadowAction,\n    SetShadowModeAction,\n    shadowToString,\n} from \"@html_builder/plugins/shadow_option_plugin\";\nimport { StyleAction } from \"@html_builder/core/core_builder_action_plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { HeaderBoxOption } from \"./header_box_option\";\nimport { HEADER_BOX } from \"./header_option_plugin\";\n\nclass HeaderBoxOptionPlugin extends Plugin {\n    static id = \"HeaderBoxOptionPlugin\";\n    static dependencies = [\"customizeWebsite\"];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(HEADER_BOX, HeaderBoxOption)],\n        builder_actions: {\n            StyleActionHeaderAction,\n            SetShadowModeHeaderAction,\n            SetShadowHeaderAction,\n        },\n    };\n}\n\nexport class StyleActionHeaderAction extends StyleAction {\n    static id = \"styleActionHeader\";\n    static dependencies = [\"customizeWebsite\", \"color\"];\n    setup() {\n        this.preview = false;\n        this.dependencies.customizeWebsite.withCustomHistory(this);\n    }\n    getValue(...args) {\n        const { params } = args[0];\n        const value = super.getValue(...args);\n        if (params.mainParam === \"border-width\") {\n            return value.replace(/(^|\\s)0px/gi, \"\").trim() || value;\n        }\n        return value;\n    }\n    async apply({ params, value }) {\n        const styleName = params.mainParam;\n\n        if (styleName === \"border-color\") {\n            return this.dependencies.customizeWebsite.customizeWebsiteColors({\n                \"menu-border-color\": value,\n            });\n        }\n        return this.dependencies.customizeWebsite.customizeWebsiteVariables({\n            [`menu-${styleName}`]: value,\n        });\n    }\n}\n\nexport class SetShadowModeHeaderAction extends SetShadowModeAction {\n    static id = \"setShadowModeHeader\";\n    static dependencies = [\"customizeWebsite\"];\n    setup() {\n        this.preview = false;\n        this.dependencies.customizeWebsite.withCustomHistory(this);\n    }\n    async apply({ value: shadowMode }) {\n        const defaultShadow = shadowMode === \"none\" ? \"none\" : getDefaultShadow(shadowMode);\n        return this.dependencies.customizeWebsite.customizeWebsiteVariables({\n            \"menu-box-shadow\": defaultShadow,\n        });\n    }\n}\n\nexport class SetShadowHeaderAction extends SetShadowAction {\n    static id = \"setShadowHeader\";\n    static dependencies = [\"customizeWebsite\"];\n    setup() {\n        this.preview = false;\n        this.dependencies.customizeWebsite.withCustomHistory(this);\n    }\n    async apply({ editingElement, params: { mainParam: attributeName }, value }) {\n        const shadow = getCurrentShadow(editingElement);\n        shadow[attributeName] = value;\n\n        return this.dependencies.customizeWebsite.customizeWebsiteVariables({\n            \"menu-box-shadow\": shadowToString(shadow),\n        });\n    }\n}\n\nregistry.category(\"website-plugins\").add(HeaderBoxOptionPlugin.id, HeaderBoxOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { basicHeaderOptionSettings } from \"./basicHeaderOptionSettings\";\n\nexport class HeaderElementsOption extends BaseOptionComponent {\n    static template = \"website.HeaderElementsOption\";\n    static dependencies = [\"customizeWebsite\"];\n\n    setup() {\n        super.setup();\n        this.customizeWebsite = this.dependencies.customizeWebsite;\n        const views = [\"website.option_header_brand_logo\", \"website.option_header_brand_name\"];\n        this.customizeWebsite.loadConfigKey({ views });\n    }\n\n    get websiteLogoParams() {\n        const views = this.customizeWebsite.getConfigKey(\"website.option_header_brand_name\")\n            ? [\"website.option_header_brand_name\"]\n            : [\"website.option_header_brand_logo\"];\n        return {\n            views,\n            resetViewArch: true,\n        };\n    }\n}\n\nObject.assign(HeaderElementsOption, basicHeaderOptionSettings);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { basicHeaderOptionSettings } from \"./basicHeaderOptionSettings\";\n\nexport class HeaderFontOption extends BaseOptionComponent {\n    static template = \"website.HeaderFontOption\";\n}\n\nObject.assign(HeaderFontOption, basicHeaderOptionSettings);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { basicHeaderOptionSettings } from \"./basicHeaderOptionSettings\";\n\nexport class HeaderIconBackgroundOption extends BaseOptionComponent {\n    static template = \"website.HeaderIconBackgroundOption\";\n}\n\nObject.assign(HeaderIconBackgroundOption, basicHeaderOptionSettings);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { onWillStart } from \"@odoo/owl\";\nimport { basicHeaderOptionSettings } from \"./basicHeaderOptionSettings\";\n\nexport class HeaderNavigationOption extends BaseOptionComponent {\n    static template = \"website.HeaderNavigationOption\";\n    static dependencies = [\"customizeWebsite\"];\n    static reloadTarget = true;\n\n    setup() {\n        super.setup();\n\n        this.keys = [\n            \"website.template_header_default\",\n            \"website.template_header_hamburger\",\n            \"website.template_header_boxed\",\n            \"website.template_header_stretch\",\n            \"website.template_header_vertical\",\n            \"website.template_header_search\",\n            \"website.template_header_sales_one\",\n            \"website.template_header_sales_two\",\n            \"website.template_header_sales_three\",\n            \"website.template_header_sales_four\",\n            \"website.template_header_sidebar\",\n        ];\n\n        this.currentActiveViews = {};\n        onWillStart(async () => {\n            this.currentActiveViews = await this.getCurrentActiveViews();\n        });\n    }\n\n    hasSomeViews(views) {\n        for (const view of views) {\n            if (this.currentActiveViews[view]) {\n                return true;\n            }\n        }\n        return false;\n    }\n    async getCurrentActiveViews() {\n        const actionParams = { views: this.keys };\n        await this.dependencies.customizeWebsite.loadConfigKey(actionParams);\n        const currentActiveViews = {};\n        for (const key of this.keys) {\n            const isActive = this.dependencies.customizeWebsite.getConfigKey(key);\n            currentActiveViews[key] = isActive;\n        }\n        return currentActiveViews;\n    }\n}\n\nObject.assign(HeaderNavigationOption, basicHeaderOptionSettings);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { HeaderNavigationOption } from \"./header_navigation_option\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { HEADER_NAVIGATION } from \"./header_option_plugin\";\n\nclass HeaderNavigationOptionPlugin extends Plugin {\n    static id = \"HeaderNavigationOptionPlugin\";\n    static dependencies = [\"customizeWebsite\"];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(HEADER_NAVIGATION, HeaderNavigationOption)],\n    };\n\n    setup() {\n        this.keys = [\n            \"website.template_header_default\",\n            \"website.template_header_hamburger\",\n            \"website.template_header_boxed\",\n            \"website.template_header_stretch\",\n            \"website.template_header_vertical\",\n            \"website.template_header_search\",\n            \"website.template_header_sales_one\",\n            \"website.template_header_sales_two\",\n            \"website.template_header_sales_three\",\n            \"website.template_header_sales_four\",\n            \"website.template_header_sidebar\",\n        ];\n    }\n\n    async getCurrentActiveViews() {\n        const actionParams = { views: this.keys };\n        await this.dependencies.customizeWebsite.loadConfigKey(actionParams);\n        const currentActiveViews = {};\n        for (const key of this.keys) {\n            const isActive = this.dependencies.customizeWebsite.getConfigKey(key);\n            currentActiveViews[key] = isActive;\n        }\n        return currentActiveViews;\n    }\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(HeaderNavigationOptionPlugin.id, HeaderNavigationOptionPlugin);\n", "import {\n    SNIPPET_SPECIFIC_END,\n    SNIPPET_SPECIFIC_NEXT,\n    splitBetween,\n} from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\nimport { HeaderElementsOption } from \"./header_elements_option\";\nimport { HeaderFontOption } from \"./header_font_option\";\nimport { HeaderTemplateOption } from \"./header_template_option\";\nimport { HeaderIconBackgroundOption } from \"./header_icon_background_option\";\nimport { HeaderTopOptions } from \"./header_top_options\";\n\nconst [\n    HEADER_TEMPLATE,\n    HEADER_FONT,\n    HEADER_BOX,\n    HEADER_NAVIGATION,\n    HEADER_ELEMENTS,\n    HEADER_ICON_BACKGROUND,\n    HEADER_END,\n    ...__ERROR_CHECK__\n] = splitBetween(SNIPPET_SPECIFIC_NEXT, SNIPPET_SPECIFIC_END, 7);\nif (__ERROR_CHECK__.length > 0) {\n    console.error(\"Wrong count in header option split\");\n}\n\nexport {\n    HEADER_TEMPLATE,\n    HEADER_FONT,\n    HEADER_BOX,\n    HEADER_NAVIGATION,\n    HEADER_ELEMENTS,\n    HEADER_ICON_BACKGROUND,\n    HEADER_END,\n};\n\nclass HeaderOptionPlugin extends Plugin {\n    static id = \"headerOption\";\n    static dependencies = [\"customizeWebsite\", \"menuDataPlugin\"];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_header_middle_buttons: [\n            {\n                Component: HeaderTopOptions,\n                editableOnly: false,\n                selector: \"#wrapwrap > header\",\n                props: {\n                    openEditMenu: () => this.dependencies.menuDataPlugin.openEditMenu(),\n                },\n            },\n        ],\n        builder_options: [\n            withSequence(HEADER_TEMPLATE, HeaderTemplateOption),\n            withSequence(HEADER_FONT, HeaderFontOption),\n            withSequence(HEADER_ELEMENTS, HeaderElementsOption),\n            withSequence(HEADER_ICON_BACKGROUND, HeaderIconBackgroundOption),\n        ],\n    };\n}\n\nregistry.category(\"website-plugins\").add(HeaderOptionPlugin.id, HeaderOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { basicHeaderOptionSettings } from \"./basicHeaderOptionSettings\";\n\nexport class HeaderTemplateOption extends BaseOptionComponent {\n    static template = \"website.HeaderTemplateOption\";\n\n    hasSomeOptions(opts) {\n        return opts.some((opt) => this.isActiveItem(opt));\n    }\n}\n\nObject.assign(HeaderTemplateOption, basicHeaderOptionSettings);\n", "import { Component } from \"@odoo/owl\";\n\nexport class HeaderTopOptions extends Component {\n    static template = \"website.HeaderTopOptions\";\n    static props = {\n        openEditMenu: Function,\n    };\n}\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { BorderConfigurator } from \"@html_builder/plugins/border_configurator_option\";\n\nexport class ImageGalleryComponent extends BaseOptionComponent {\n    static template = \"website.ImageGalleryOption\";\n    static selector = \".s_image_gallery\";\n\n    static components = { BorderConfigurator };\n\n    setup() {\n        super.setup();\n        this.state = useDomState((editingElement) => ({\n            isSlideShow: editingElement.classList.contains(\"o_slideshow\"),\n        }));\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { loadImageInfo } from \"@html_editor/utils/image_processing\";\nimport { ImageGalleryComponent } from \"./image_gallery_option\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { updateCarouselIndicators } from \"../carousel_option_plugin\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { SNIPPET_SPECIFIC, SNIPPET_SPECIFIC_END } from \"@html_builder/utils/option_sequence\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { forwardToThumbnail } from \"@html_builder/utils/utils_css\";\n\n/**\n * @typedef { Object } ImageGalleryOptionShared\n * @property { ImageGalleryOption['getColumns'] } getColumns\n * @property { ImageGalleryOption['getMode'] } getMode\n * @property { ImageGalleryOption['processImage'] } processImage\n * @property { ImageGalleryOption['restoreSelection'] } restoreSelection\n * @property { ImageGalleryOption['setImages'] } setImages\n */\n\nexport class ImageGalleryImagesOption extends BaseOptionComponent {\n    static template = \"website.ImageGalleryImagesOption\";\n    static selector = \".s_image_gallery\";\n}\n\nclass ImageGalleryOption extends Plugin {\n    static id = \"imageGalleryOption\";\n    static dependencies = [\n        \"media\",\n        \"dom\",\n        \"history\",\n        \"operation\",\n        \"selection\",\n        \"builderOptions\",\n        \"imagePostProcess\",\n    ];\n    static shared = [\"processImages\", \"getMode\", \"setImages\", \"restoreSelection\", \"getColumns\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(SNIPPET_SPECIFIC, ImageGalleryImagesOption),\n            withSequence(SNIPPET_SPECIFIC_END, ImageGalleryComponent),\n        ],\n        builder_actions: {\n            AddImageAction,\n            RemoveAllImagesAction,\n            SetImageGalleryLayoutAction,\n            SetImageGalleryColumnsAction,\n            SetCarouselSpeedAction,\n        },\n        system_classes: [\"o_empty_gallery_alert\"],\n        get_gallery_items_handlers: this.getGalleryItems.bind(this),\n        reorder_items_handlers: this.reorderGalleryItems.bind(this),\n        on_will_remove_handlers: this.onWillRemove.bind(this),\n        on_removed_handlers: this.onRemoved.bind(this),\n        on_replaced_media_handlers: ({ newMediaEl }) => this.updateCarouselThumbnail(newMediaEl),\n        on_image_updated_handlers: ({ imageEl }) => this.updateCarouselThumbnail(imageEl),\n        on_image_saved_handlers: ({ imageEl }) => this.updateCarouselThumbnail(imageEl),\n        on_snippet_dropped_handlers: ({ snippetEl }) => {\n            const carousels = snippetEl.querySelectorAll(\".s_image_gallery .carousel\");\n            this.addCarouselListener(carousels);\n            this.addUniqueIds(carousels);\n        },\n        on_cloned_handlers: ({ cloneEl }) => {\n            const carousels = cloneEl.querySelectorAll(\".s_image_gallery .carousel\");\n            this.addUniqueIds(carousels);\n        },\n    };\n\n    setup() {\n        const slideshowCarousels = this.document.querySelectorAll(\".s_image_gallery .carousel\");\n        this.addCarouselListener(slideshowCarousels);\n    }\n\n    addUniqueIds(carousels) {\n        for (const carousel of carousels) {\n            const id = uniqueId(\"slideshow_\");\n            carousel.id = id;\n            const controllerButtons = carousel.querySelectorAll(\".o_carousel_controllers button\");\n            for (const button of controllerButtons) {\n                button.setAttribute(\"data-bs-target\", `#${id}`);\n            }\n        }\n    }\n\n    addCarouselListener(slideshowCarousels) {\n        for (const carousel of slideshowCarousels) {\n            this.addDomListener(carousel, \"slid.bs.carousel\", this.onCarouselSlid);\n        }\n    }\n\n    restoreSelection(imageToSelect, isPreviewing) {\n        if (imageToSelect && !isPreviewing) {\n            // Activate the containers of the equivalent cloned image.\n            this.dependencies.builderOptions.setNextTarget(imageToSelect);\n        }\n    }\n\n    /**\n     * Gets the gallery images to reorder.\n     *\n     * @param {HTMLElement} activeItemEl the current active image\n     * @param {String} optionName\n     * @returns {Array<HTMLElement>}\n     */\n    getGalleryItems(activeItemEl, optionName) {\n        let itemEls = [];\n        if (optionName === \"GalleryImageList\") {\n            const galleryEl = activeItemEl.closest(\".s_image_gallery\");\n            const containerEl = this.getContainer(galleryEl);\n            itemEls = this.getImages(containerEl);\n        }\n        return itemEls;\n    }\n\n    /**\n     * Updates the DOM with the reordered images.\n     *\n     * @param {HTMLElement} activeItemEl the active item\n     * @param {Array<HTMLElement>} itemEls the reordered elements\n     * @param {String} optionName\n     */\n    reorderGalleryItems(activeItemEl, itemEls, optionName) {\n        if (optionName === \"GalleryImageList\") {\n            const galleryEl = activeItemEl.closest(\".s_image_gallery\");\n\n            // Update the content with the new order.\n            itemEls.forEach((img, i) => (img.dataset.index = i));\n            const mode = this.getMode(galleryEl);\n            this.setImages(galleryEl, mode, itemEls);\n\n            // Update the active slide if it is a carousel.\n            if (mode === \"slideshow\") {\n                const newPosition = itemEls.indexOf(activeItemEl);\n                const carouselEl = galleryEl.querySelector(\".carousel\");\n                const carouselItemEls = carouselEl.querySelectorAll(\".carousel-item\");\n                carouselItemEls.forEach((itemEl, i) => {\n                    itemEl.classList.toggle(\"active\", i === newPosition);\n                });\n                updateCarouselIndicators(carouselEl, newPosition);\n\n                // Activate the active image.\n                const activeImageEl = galleryEl.querySelector(\".carousel-item.active img\");\n                this.dependencies.builderOptions.setNextTarget(activeImageEl);\n            }\n        }\n    }\n\n    /**\n     * Set the images in the gallery by following the wanted layout\n     * @param {Element} imageGalleryElement\n     * @param {String('slideshow'|'masonry'|'grid'|'nomode')} mode\n     * @param {Element[]} images\n     */\n    setImages(imageGalleryElement, mode, images) {\n        if (mode !== this.getMode(imageGalleryElement)) {\n            imageGalleryElement.classList.remove(\"o_nomode\", \"o_masonry\", \"o_grid\", \"o_slideshow\");\n            imageGalleryElement.classList.add(`o_${mode}`);\n        }\n        switch (mode) {\n            case \"masonry\":\n                this.masonry(imageGalleryElement, images);\n                break;\n            case \"grid\":\n                this.grid(imageGalleryElement, images);\n                break;\n            case \"nomode\":\n                this.nomode(imageGalleryElement, images);\n                break;\n            case \"slideshow\":\n                this.slideshow(imageGalleryElement, images);\n                break;\n        }\n    }\n\n    /**\n     * @param {Element} imageGalleryElement\n     * @param {Element[]} images\n     */\n    masonry(imageGalleryElement, images) {\n        const columnsNumber = this.getColumns(imageGalleryElement);\n        const colClass = \"col-lg-\" + 12 / columnsNumber;\n        const columns = [];\n\n        const row = document.createElement(\"div\");\n        row.classList.add(\"row\", \"s_nb_column_fixed\");\n        this.getContainer(imageGalleryElement).replaceChildren(row);\n\n        for (let i = 0; i < columnsNumber; i++) {\n            const column = document.createElement(\"div\");\n            column.classList.add(\"o_masonry_col\", \"o_snippet_not_selectable\", colClass);\n            row.append(column);\n            columns.push(column);\n        }\n\n        // Dispatch images in columns by always putting the next one in the smallest height column\n        for (const imageEl of images) {\n            let min = Infinity;\n            let smallestColEl;\n            for (const colEl of columns) {\n                const imagesInCol = colEl.querySelectorAll(\"img\");\n                const lastImageRect =\n                    imagesInCol.length &&\n                    imagesInCol[imagesInCol.length - 1].getBoundingClientRect();\n                const height = lastImageRect\n                    ? Math.round(lastImageRect.top + lastImageRect.height)\n                    : 0;\n                if (height < min) {\n                    min = height;\n                    smallestColEl = colEl;\n                }\n            }\n            smallestColEl.append(imageEl);\n        }\n    }\n\n    /**\n     * Displays the images with the \"grid\" layout.\n     *\n     * @param {Element} imageGalleryElement\n     * @param {Element[]} images\n     */\n    grid(imageGalleryElement, images) {\n        const columnsNumber = this.getColumns(imageGalleryElement);\n        const colClass = \"col-lg-\" + 12 / columnsNumber;\n\n        const container = this.getContainer(imageGalleryElement);\n        let row = document.createElement(\"div\");\n        row.classList.add(\"row\", \"s_nb_column_fixed\");\n        container.replaceChildren(row);\n\n        for (const [index, img] of images.entries()) {\n            const col = this.document.createElement(\"div\");\n            col.classList.add(colClass);\n            col.appendChild(img);\n            row.appendChild(col);\n            if ((index + 1) % columnsNumber === 0) {\n                row = document.createElement(\"div\");\n                row.classList.add(\"row\", \"s_nb_column_fixed\");\n                container.appendChild(row);\n            }\n        }\n    }\n\n    nomode(imageGalleryElement, images) {\n        const row = this.document.createElement(\"div\");\n        row.classList.add(\"row\", \"s_nb_column_fixed\");\n        const container = this.getContainer(imageGalleryElement);\n        container.replaceChildren(row);\n        for (const img of images) {\n            let wrapClass = \"col-lg-3\";\n            if (img.width >= img.height * 2 || img.width > 600) {\n                wrapClass = \"col-lg-6\";\n            }\n\n            const wrap = this.document.createElement(\"div\");\n            wrap.classList.add(wrapClass);\n            wrap.appendChild(img);\n            row.appendChild(wrap);\n        }\n    }\n\n    slideshow(imageGalleryElement, images) {\n        const container = this.getContainer(imageGalleryElement);\n        const currentInterval = imageGalleryElement.querySelector(\".carousel\")?.dataset.bsInterval;\n        const carouselEl = imageGalleryElement.querySelector(\".carousel\");\n        const colorContrast =\n            carouselEl && carouselEl.classList.contains(\"carousel-dark\") ? \"carousel-dark\" : \" \";\n        const slideshowEl = renderToElement(\"website.s_image_gallery_slideshow\", {\n            images: images,\n            index: 0,\n            interval: currentInterval || 0,\n            ride: !currentInterval ? \"false\" : \"carousel\",\n            id: \"slideshow_\" + new Date().getTime(),\n            colorContrast,\n            copyAttributes: true,\n        });\n        if (carouselEl) {\n            carouselEl.removeEventListener(\"slid.bs.carousel\", this.onCarouselSlid);\n        }\n        container.replaceChildren(slideshowEl);\n        slideshowEl.querySelectorAll(\"img\").forEach((img, index) => {\n            img.setAttribute(\"data-index\", index);\n        });\n        if (images.length) {\n            imageGalleryElement.style.height = window.innerHeight * 0.7 + \"px\";\n            slideshowEl\n                .querySelector(\".carousel .o_carousel_controllers\")\n                ?.classList.remove(\"d-none\");\n            slideshowEl.querySelector(\".carousel .carousel-inner\")?.classList.remove(\"d-none\");\n        } else {\n            imageGalleryElement.style.removeProperty(\"height\");\n            slideshowEl.querySelector(\".carousel .o_carousel_controllers\")?.classList.add(\"d-none\");\n            slideshowEl.querySelector(\".carousel .carousel-inner\")?.classList.add(\"d-none\");\n        }\n        this.addDomListener(slideshowEl, \"slid.bs.carousel\", this.onCarouselSlid);\n    }\n\n    onCarouselSlid(ev) {\n        // When the carousel slides, update the builder options to select the active image\n        const activeImageEl = ev.target.querySelector(\".carousel-item.active img\");\n        this.dependencies.builderOptions.updateContainers(activeImageEl);\n    }\n\n    async processImages(editingElement, newImages = []) {\n        await this.transformImagesToWebp(newImages);\n        this.setImageProperties(editingElement, newImages);\n        const { clonedImgs, imageToSelect } = await this.cloneContainerImages(editingElement);\n        return { images: [...clonedImgs, ...newImages], imageToSelect };\n    }\n\n    setImageProperties(imageGalleryElement, images) {\n        const lastImage = this.getImages(imageGalleryElement).at(-1);\n        let lastIndex = lastImage ? this.getIndex(lastImage) : -1;\n        for (const image of images) {\n            image.classList.add(\n                \"d-block\",\n                \"mh-100\",\n                \"mw-100\",\n                \"mx-auto\",\n                \"rounded\",\n                \"object-fit-cover\"\n            );\n            image.dataset.index = ++lastIndex;\n        }\n    }\n\n    async transformImagesToWebp(images) {\n        const process = async (img) => {\n            const newDataset = await loadImageInfo(img);\n            const { mimetypeBeforeConversion } = { ...img.dataset, ...newDataset };\n            if (\n                mimetypeBeforeConversion &&\n                ![\"image/gif\", \"image/svg+xml\", \"image/webp\"].includes(mimetypeBeforeConversion)\n            ) {\n                // Convert to webp but keep original width.\n                const update = await this.dependencies.imagePostProcess.processImage({\n                    img,\n                    newDataset: {\n                        formatMimetype: \"image/webp\",\n                        ...newDataset,\n                    },\n                });\n                update();\n            }\n        };\n        return await Promise.all(images.map(process));\n    }\n\n    async cloneContainerImages(imageGalleryElement) {\n        const imagesHolder = this.getImageHolder(imageGalleryElement);\n        const clonedImgs = [];\n        const imgLoaded = [];\n        let imageToSelect;\n        const currentContainers = this.dependencies.builderOptions.getContainers();\n        for (const image of imagesHolder) {\n            // Only on Chrome: appended images are sometimes invisible\n            // and not correctly loaded from cache, we use a clone of the\n            // image to force the loading.\n            const newImg = image.cloneNode(true);\n            const imgEl = newImg.tagName === \"IMG\" ? newImg : newImg.querySelector(\":scope > img\");\n            imgEl.loading = \"eager\";\n            imgLoaded.push(\n                imgEl.decode().then(() => {\n                    imgEl.loading = \"lazy\";\n                })\n            );\n            if (currentContainers.at(-1)?.element === image) {\n                imageToSelect = newImg;\n            }\n            clonedImgs.push(newImg);\n        }\n        await Promise.all(imgLoaded);\n        return { clonedImgs, imageToSelect };\n    }\n\n    /**\n     * Get the image target's layout mode (slideshow, masonry, grid or nomode).\n     *\n     * @returns {String('slideshow'|'masonry'|'grid'|'nomode')}\n     */\n    getMode(imageGalleryElement) {\n        if (imageGalleryElement.classList.contains(\"o_masonry\")) {\n            return \"masonry\";\n        }\n        if (imageGalleryElement.classList.contains(\"o_grid\")) {\n            return \"grid\";\n        }\n        if (imageGalleryElement.classList.contains(\"o_nomode\")) {\n            return \"nomode\";\n        }\n        return \"slideshow\";\n    }\n\n    getImages(currentContainer) {\n        const imgs = currentContainer.querySelectorAll(\"img\");\n        return [...imgs].sort((imgA, imgB) => this.getIndex(imgA) - this.getIndex(imgB));\n    }\n\n    getIndex(img) {\n        return parseInt(img.dataset.index) || 0;\n    }\n\n    getImageHolder(currentContainer) {\n        const images = this.getImages(currentContainer);\n        return [...images].map((image) => image.closest(\"a\") || image);\n    }\n\n    getColumns(imageGalleryElement) {\n        return parseInt(imageGalleryElement.dataset.columns) || 3;\n    }\n\n    getContainer(imageGalleryElement) {\n        return imageGalleryElement.querySelector(\n            \".container, .container-fluid, .o_container_small\"\n        );\n    }\n\n    onWillRemove(toRemoveEl) {\n        // If the removed element is an image from a gallery, store the gallery\n        // element for `onRemoved`.\n        if (toRemoveEl.matches(\".s_image_gallery img\")) {\n            this.imageRemovedGalleryElement = toRemoveEl.closest(\".s_image_gallery\");\n        }\n    }\n\n    onRemoved() {\n        // If the removed element is an image from a gallery, relayout the\n        // gallery.\n        if (this.imageRemovedGalleryElement) {\n            const mode = this.getMode(this.imageRemovedGalleryElement);\n            const images = this.getImages(this.imageRemovedGalleryElement);\n            this.setImages(this.imageRemovedGalleryElement, mode, images);\n            this.imageRemovedGalleryElement = undefined;\n        }\n    }\n\n    updateCarouselThumbnail(mediaEl) {\n        if (mediaEl.matches(\".s_image_gallery img\")) {\n            forwardToThumbnail(mediaEl);\n        }\n    }\n}\n\nexport class AddImageAction extends BuilderAction {\n    static id = \"addImage\";\n    static dependencies = [\"media\", \"imageGalleryOption\"];\n    async load({ editingElement }) {\n        let selectedImages;\n        await new Promise((resolve) => {\n            const onClose = this.dependencies.media.openMediaDialog({\n                onlyImages: true,\n                multiImages: true,\n                save: (images) => {\n                    selectedImages = images;\n                    resolve();\n                },\n            });\n            onClose.then(resolve);\n        });\n        if (!selectedImages) {\n            return [];\n        }\n        return this.dependencies.imageGalleryOption.processImages(editingElement, selectedImages);\n    }\n    apply({ editingElement, loadResult: { images } }) {\n        if (images && images.length) {\n            const mode = this.dependencies.imageGalleryOption.getMode(editingElement);\n            this.dependencies.imageGalleryOption.setImages(editingElement, mode, images);\n        }\n    }\n}\nexport class RemoveAllImagesAction extends BuilderAction {\n    static id = \"removeAllImages\";\n    static dependencies = [\"imageGalleryOption\"];\n    apply({ editingElement: el }) {\n        const mode = this.dependencies.imageGalleryOption.getMode(el);\n        this.dependencies.imageGalleryOption.setImages(el, mode, []);\n    }\n}\nexport class SetImageGalleryLayoutAction extends BuilderAction {\n    static id = \"setImageGalleryLayout\";\n    static dependencies = [\"imageGalleryOption\"];\n    load({ editingElement }) {\n        return this.dependencies.imageGalleryOption.processImages(editingElement);\n    }\n    apply({ isPreviewing, editingElement, params: { mainParam: mode }, loadResult }) {\n        if (mode !== this.dependencies.imageGalleryOption.getMode(editingElement)) {\n            this.dependencies.imageGalleryOption.setImages(editingElement, mode, loadResult.images);\n            this.dependencies.imageGalleryOption.restoreSelection(\n                loadResult.imageToSelect,\n                isPreviewing\n            );\n        }\n    }\n    isApplied({ editingElement, params: { mainParam: mode } }) {\n        return mode === this.dependencies.imageGalleryOption.getMode(editingElement);\n    }\n}\nexport class SetImageGalleryColumnsAction extends BuilderAction {\n    static id = \"setImageGalleryColumns\";\n    static dependencies = [\"imageGalleryOption\"];\n    load({ editingElement }) {\n        return this.dependencies.imageGalleryOption.processImages(editingElement);\n    }\n    apply({ isPreviewing, editingElement, params: { mainParam: columns }, loadResult }) {\n        if (columns !== this.dependencies.imageGalleryOption.getColumns(editingElement)) {\n            editingElement.dataset.columns = columns;\n            this.dependencies.imageGalleryOption.setImages(\n                editingElement,\n                this.dependencies.imageGalleryOption.getMode(editingElement),\n                loadResult.images\n            );\n            this.dependencies.imageGalleryOption.restoreSelection(\n                loadResult.imageToSelect,\n                isPreviewing\n            );\n        }\n    }\n    isApplied({ editingElement, params: { mainParam: columns } }) {\n        return columns === this.dependencies.imageGalleryOption.getColumns(editingElement);\n    }\n}\n\nexport class SetCarouselSpeedAction extends BuilderAction {\n    static id = \"setCarouselSpeed\";\n    apply({ editingElement, value }) {\n        editingElement.dataset.bsInterval = value * 1000;\n    }\n    getValue({ editingElement }) {\n        return editingElement.dataset.bsInterval / 1000;\n    }\n}\n\nregistry.category(\"website-plugins\").add(ImageGalleryOption.id, ImageGalleryOption);\n", "import { SNIPPET_SPECIFIC_END } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { getCommonAncestor, selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\n/**\n * @typedef { Object } InstagramOptionShared\n * @property { InstagramOptionPlugin['instagramPageNameFromUrl'] } instagramPageNameFromUrl\n */\n\nexport class InstagramOption extends BaseOptionComponent {\n    static template = \"website.InstagramOption\";\n    static selector = \".s_instagram_page\";\n}\n\nclass InstagramOptionPlugin extends Plugin {\n    static id = \"instagramOption\";\n    static dependencies = [\"history\"];\n    static shared = [\"instagramPageNameFromUrl\"];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(SNIPPET_SPECIFIC_END, InstagramOption)],\n        builder_actions: {\n            InstagramPageAction,\n        },\n        normalize_handlers: this.normalize.bind(this),\n    };\n\n    setup() {\n        this.instagramUrlStr = \"instagram.com/\";\n    }\n\n    normalize(root) {\n        const nodes = [\n            ...selectElements(root, \".s_instagram_page[data-instagram-page-is-default]\"),\n        ];\n        if (nodes.length) {\n            this.loadAndSetPage(nodes);\n        }\n    }\n\n    async loadAndSetPage(nodes) {\n        // TODO: look in shared cache with social info: was SocialMediaOption.getDbSocialValuesCache()\n        if (this.instagramUrl) {\n            this.setPage(nodes);\n            return;\n        }\n        // Fetches the default url for instagram page from website config\n        const res = await this.services.orm.read(\n            \"website\",\n            [this.services.website.currentWebsite.id],\n            [\"social_instagram\"]\n        );\n        if (res && res[0].social_instagram) {\n            this.instagramUrl = this.instagramPageNameFromUrl(res[0].social_instagram);\n\n            // WARNING: the call to ignoreDOMMutations is very dangerous,\n            // and should be avoided in most cases (if you think you need those, ask html_editor team)\n            const hasChanged = this.dependencies.history.ignoreDOMMutations(() =>\n                this.setPage(nodes)\n            );\n\n            if (hasChanged) {\n                const commonAncestor = getCommonAncestor(nodes, this.editable);\n                this.dispatchTo(\"content_manually_updated_handlers\", commonAncestor);\n                this.config.onChange({ isPreviewing: false });\n            }\n        }\n    }\n\n    setPage(nodes) {\n        let hasChanged = false;\n        for (const element of nodes) {\n            if (element.dataset.instagramPageIsDefault) {\n                delete element.dataset.instagramPageIsDefault;\n                if (this.instagramUrl) {\n                    element.dataset.instagramPage = this.instagramUrl;\n                }\n                hasChanged = true;\n            }\n        }\n        return hasChanged;\n    }\n\n    /**\n     * Returns the instagram page name from the given url.\n     *\n     * @private\n     * @param {string} url\n     * @returns {string|undefined}\n     */\n    instagramPageNameFromUrl(url) {\n        const pageName = url.split(this.instagramUrlStr)[1];\n        if (\n            !pageName ||\n            pageName.includes(\"?\") ||\n            pageName.includes(\"#\") ||\n            (pageName.includes(\"/\") && pageName.split(\"/\")[1].length > 0)\n        ) {\n            return;\n        }\n        return pageName.split(\"/\")[0];\n    }\n}\n\nexport class InstagramPageAction extends BuilderAction {\n    static id = \"instagramPage\";\n    static dependencies = [\"instagramOption\"];\n    getValue({ editingElement }) {\n        return editingElement.dataset[\"instagramPage\"];\n    }\n    apply({ editingElement, value }) {\n        delete editingElement.dataset.instagramPageIsDefault;\n        if (value.includes(this.instagramUrlStr)) {\n            value = this.dependencies.instagramOption.instagramPageNameFromUrl(value) || \"\";\n        }\n        editingElement.dataset[\"instagramPage\"] = value;\n        if (value === \"\") {\n            this.services.notification.add(_t(\"The Instagram page name is not valid\"), {\n                type: \"warning\",\n            });\n        }\n    }\n}\n\nregistry.category(\"website-plugins\").add(InstagramOptionPlugin.id, InstagramOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { after } from \"@html_builder/utils/option_sequence\";\nimport { HEADER_BOX } from \"./header/header_option_plugin\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class LanguageSelectorOption extends BaseOptionComponent {\n    static template = \"website.LanguageSelectorOption\";\n    static selector = \"#wrapwrap > header nav.navbar .o_header_language_selector\";\n    static groups = [\"website.group_website_designer\"];\n    static editableOnly = false;\n    static reloadTarget = true;\n}\n\nconst LANGUAGE_SELECTOR = after(HEADER_BOX);\nclass LanguageSelectorOptionPlugin extends Plugin {\n    static id = \"languageSelectorOption\";\n    static dependencies = [\"builderActions\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(LANGUAGE_SELECTOR, LanguageSelectorOption)],\n    };\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(LanguageSelectorOptionPlugin.id, LanguageSelectorOptionPlugin);\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { generateGMapLink } from \"@website/js/utils\";\n\nexport class MapOption extends BaseOptionComponent {\n    static template = \"website.mapOption\";\n    static selector = \".s_map\";\n}\n\nclass MapOptionPlugin extends Plugin {\n    static id = \"mapOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [MapOption],\n        so_content_addition_selector: [\".s_map\"],\n        builder_actions: {\n            MapUpdateSrcAction,\n            MapDescriptionAction,\n        },\n        // TODO remove when the snippet will have a \"Height\" option.\n        keep_overlay_options: (el) => el.matches(\".s_map\"),\n    };\n}\n\nexport class MapUpdateSrcAction extends BuilderAction {\n    static id = \"mapUpdateSrc\";\n    apply({ editingElement }) {\n        const embedded = editingElement.querySelector(\".s_map_embedded\");\n\n        if (editingElement.dataset.mapAddress) {\n            const url = generateGMapLink(editingElement.dataset);\n            if (url !== embedded.getAttribute(\"src\")) {\n                embedded.setAttribute(\"src\", url);\n            }\n        } else {\n            embedded.setAttribute(\"src\", \"about:blank\");\n        }\n        embedded.classList.toggle(\"d-none\", !editingElement.dataset.mapAddress);\n        editingElement\n            .querySelector(\".missing_option_warning\")\n            .classList.toggle(\"d-none\", !!editingElement.dataset.mapAddress);\n    }\n}\nexport class MapDescriptionAction extends BuilderAction {\n    static id = \"mapDescription\";\n    isApplied({ editingElement }) {\n        return editingElement.querySelector(\".description\") !== null;\n    }\n    apply({ editingElement }) {\n        editingElement.appendChild(\n            document.createRange().createContextualFragment(\n                `<div class=\"description\">\n                    <strong>${_t(\"Visit us:\")}</strong>\n                    ${_t(\"Our office is open Monday \u2013 Friday 8:30 a.m. \u2013 4:00 p.m.\")}\n                </div>`\n            )\n        );\n    }\n    clean({ editingElement }) {\n        editingElement.querySelector(\".description\").remove();\n    }\n}\n\nregistry.category(\"website-plugins\").add(MapOptionPlugin.id, MapOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { BorderConfigurator } from \"@html_builder/plugins/border_configurator_option\";\nimport { ShadowOption } from \"@html_builder/plugins/shadow_option\";\nimport { BaseWebsiteBackgroundOption } from \"@website/builder/plugins/options/background_option\";\n\n// TODO: BorderConfigurator and ShadowOption directly in BaseOptionComponent ?\nexport class MediaListItemOption extends BaseOptionComponent {\n    static template = \"website.MediaListItemOption\";\n    static selector = \".s_media_list_item\";\n    static components = {\n        BorderConfigurator,\n        ShadowOption,\n        BaseWebsiteBackgroundOption,\n    };\n}\n", "import { BEGIN, END } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\nimport { MediaListItemOption } from \"./media_list_item_option\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class MediaListOption extends BaseOptionComponent {\n    static template = \"website.MediaListOption\";\n    static selector = \".s_media_list\";\n}\n\nclass MediaListOptionPlugin extends Plugin {\n    static id = \"mediaListOption\";\n    mediaListItemOptionSelector = \".s_media_list_item\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(BEGIN, MediaListOption),\n            withSequence(END, MediaListItemOption),\n        ],\n        builder_actions: {\n            SetMediaLayoutAction,\n        },\n        mark_color_level_selector_params: [\n            { selector: MediaListItemOption.selector, applyTo: \":scope > .row\" },\n        ],\n    };\n}\n\nexport class SetMediaLayoutAction extends BuilderAction {\n    static id = \"setMediaLayout\";\n    isApplied({ editingElement, value }) {\n        const image = editingElement.querySelector(\".s_media_list_img_wrapper\");\n        return image.classList.contains(`col-lg-${value}`);\n    }\n    apply({ editingElement, value }) {\n        const image = editingElement.querySelector(\".s_media_list_img_wrapper\");\n        const content = editingElement.querySelector(\".s_media_list_body\");\n        image.classList.add(`col-lg-${value}`);\n        content.classList.add(`col-lg-${12 - value}`);\n    }\n    clean({ editingElement, value }) {\n        const image = editingElement.querySelector(\".s_media_list_img_wrapper\");\n        const content = editingElement.querySelector(\".s_media_list_body\");\n        image.classList.remove(`col-lg-${value}`);\n        content.classList.remove(`col-lg-${12 - value}`);\n    }\n}\n\nregistry.category(\"website-plugins\").add(MediaListOptionPlugin.id, MediaListOptionPlugin);\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { getCSSVariableValue, getHtmlStyle } from \"@html_editor/utils/formatting\";\n\nexport class MegaMenuOption extends BaseOptionComponent {\n    static template = \"website.MegaMenuOption\";\n    static dependencies = [\"megaMenuOptionPlugin\"];\n    static selector = \".o_mega_menu\";\n\n    setup() {\n        super.setup();\n        const { getTemplatePrefix } = this.dependencies.megaMenuOptionPlugin;\n        this.state = useDomState((el) => ({\n            templatePrefix: getTemplatePrefix(el),\n        }));\n    }\n\n    hasHeaderTemplates(headerTemplates) {\n        const currentHeaderTemplate = getCSSVariableValue(\n            \"header-template\",\n            getHtmlStyle(this.env.editor.document)\n        );\n        return headerTemplates.includes(currentHeaderTemplate.slice(1, -1));\n    }\n}\n", "import { MegaMenuOption } from \"@website/builder/plugins/options/mega_menu_option\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { SNIPPET_SPECIFIC_NEXT } from \"@html_builder/utils/option_sequence\";\n\n/**\n * @typedef { Object } MegaMenuOptionShared\n * @property { MegaMenuOptionPlugin['getTemplatePrefix'] } getTemplatePrefix\n */\n\nexport class MegaMenuOptionPlugin extends Plugin {\n    static id = \"megaMenuOptionPlugin\";\n    static dependencies = [];\n    static shared = [\"getTemplatePrefix\"];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(SNIPPET_SPECIFIC_NEXT, MegaMenuOption)],\n        dropzone_selector: {\n            selector: \".o_mega_menu .nav > .nav-link\",\n            dropIn: \".o_mega_menu nav\",\n            dropNear: \".o_mega_menu .nav-link\",\n        },\n        save_handlers: this.saveMegaMenuClasses.bind(this),\n        no_parent_containers: \".o_mega_menu\",\n        is_unremovable_selector: \".o_mega_menu > section\",\n        unsplittable_node_predicates: (node) =>\n            node?.nodeType === Node.ELEMENT_NODE && node.matches(\".o_mega_menu .nav > .nav-link\"), //avoid merge\n    };\n\n    getTemplatePrefix() {\n        return \"website.\";\n    }\n\n    async saveMegaMenuClasses() {\n        const proms = [];\n        for (const megaMenuEl of this.editable.querySelectorAll(\n            \"[data-oe-field='mega_menu_content']\"\n        )) {\n            // On top of saving the mega menu content like any other field\n            // content, we must save the custom classes that were set on the\n            // menu itself.\n            const classes = [...megaMenuEl.classList].filter(\n                (megaMenuClass) =>\n                    ![\"dropdown-menu\", \"o_mega_menu\", \"o_editable\"].includes(megaMenuClass)\n            );\n\n            proms.push(\n                this.services.orm.write(\"website.menu\", [parseInt(megaMenuEl.dataset.oeId)], {\n                    mega_menu_classes: classes.join(\" \"),\n                })\n            );\n        }\n        await Promise.all(proms);\n    }\n}\n\nregistry.category(\"website-plugins\").add(MegaMenuOptionPlugin.id, MegaMenuOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { SNIPPET_SPECIFIC_NEXT } from \"@html_builder/utils/option_sequence\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class NavbarLogoOption extends BaseOptionComponent {\n    static template = \"website.NavbarLogoOption\";\n    static selector = \"#wrapwrap > header nav.navbar .navbar-brand\";\n    static title = _t(\"Navbar Logo\");\n    static groups = [\"website.group_website_designer\"];\n    static editableOnly = false;\n}\n\nclass NavbarLogoOptionPlugin extends Plugin {\n    static id = \"navbarLogoOptionPlugin\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(SNIPPET_SPECIFIC_NEXT, NavbarLogoOption)],\n    };\n}\n\nregistry.category(\"website-plugins\").add(NavbarLogoOptionPlugin.id, NavbarLogoOptionPlugin);\n", "import { useDomState } from \"@html_builder/core/utils\";\nimport { useOperation } from \"@html_builder/core/operation_plugin\";\nimport { Component } from \"@odoo/owl\";\n\nexport class NavTabsHeaderMiddleButtons extends Component {\n    static template = \"website.NavTabsHeaderMiddleButtons\";\n    static props = {\n        addItem: Function,\n        removeItem: Function,\n    };\n\n    setup() {\n        this.state = useDomState((editingElement) => {\n            const navEl = editingElement.querySelector(\".nav\");\n            return {\n                tabEls: navEl.querySelectorAll(\".nav-item\"),\n            };\n        });\n\n        this.callOperation = useOperation();\n    }\n\n    addItem() {\n        this.callOperation(async () => {\n            await this.props.addItem(this.env.getEditingElement());\n        });\n    }\n\n    removeItem() {\n        this.callOperation(() => {\n            this.props.removeItem(this.env.getEditingElement());\n        });\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { getElementsWithOption } from \"@html_builder/utils/utils\";\nimport { NavTabsHeaderMiddleButtons } from \"./navtabs_header_buttons\";\n\nconst tabsSectionSelector = \"section.s_tabs, section.s_tabs_images\";\n\nclass NavTabsOptionPlugin extends Plugin {\n    static id = \"navTabsOption\";\n    static dependencies = [\"clone\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_header_middle_buttons: {\n            Component: NavTabsHeaderMiddleButtons,\n            selector: tabsSectionSelector,\n            props: {\n                addItem: async (editingElement) => await this.addItem(editingElement),\n                removeItem: (editingElement) => this.removeItem(editingElement),\n            },\n        },\n        on_snippet_dropped_handlers: this.onSnippetDropped.bind(this),\n        on_cloned_handlers: this.onCloned.bind(this),\n    };\n\n    getNavLinkEls(editingElement) {\n        const navEl = editingElement.querySelector(\".nav\");\n        return navEl.querySelectorAll(\".nav-item .nav-link\");\n    }\n    getPaneEls(editingElement) {\n        const tabContentEl = editingElement.querySelector(\".tab-content\");\n        return tabContentEl.querySelectorAll(\":scope > .tab-pane\");\n    }\n    getActiveLinkEl(editingElement) {\n        const navEl = editingElement.querySelector(\".nav\");\n        return navEl.querySelector(\".nav-link.active\");\n    }\n    getActivePaneEl(editingElement) {\n        const tabContentEl = editingElement.querySelector(\".tab-content\");\n        return tabContentEl.querySelector(\":scope > .tab-pane.active\");\n    }\n\n    showTab(navLinkEl, paneEl) {\n        this.window.Tab.getOrCreateInstance(navLinkEl).show();\n        // Immediately show the pane so the history remains consistent.\n        paneEl.classList.add(\"show\");\n    }\n\n    async addItem(editingElement) {\n        const activeNavItemEl = this.getActiveLinkEl(editingElement).parentElement;\n        const activePaneEl = this.getActivePaneEl(editingElement);\n\n        const newPaneEl = await this.dependencies.clone.cloneElement(activePaneEl);\n        const newNavItemEl = activeNavItemEl.cloneNode(true);\n        activeNavItemEl.after(newNavItemEl);\n        // To make sure the DOM is clean and correct, leave it to Bootstrap to\n        // update it. We leave `.active` only on the former active elements.\n        newPaneEl.classList.remove(\"active\", \"show\");\n        newNavItemEl.firstElementChild.classList.remove(\"active\");\n        this.generateUniqueIDs(editingElement);\n        this.showTab(newNavItemEl.querySelector(\".nav-link\"), newPaneEl);\n    }\n\n    removeItem(editingElement) {\n        const activeLinkEl = this.getActiveLinkEl(editingElement);\n        const activePaneEl = this.getActivePaneEl(editingElement);\n        // Show the next tab.\n        const navLinkEls = [...this.getNavLinkEls(editingElement)];\n        const index = (navLinkEls.indexOf(activeLinkEl) + 1) % navLinkEls.length;\n        const nextActiveLinkEl = navLinkEls[index];\n        const nextActivePaneEl = [...this.getPaneEls(editingElement)][index];\n        this.showTab(nextActiveLinkEl, nextActivePaneEl);\n        // Remove the tab.\n        activeLinkEl.parentElement.remove();\n        activePaneEl.remove();\n    }\n\n    onSnippetDropped({ snippetEl }) {\n        const tabsEls = getElementsWithOption(snippetEl, tabsSectionSelector);\n        for (const tabsEl of tabsEls) {\n            this.generateUniqueIDs(tabsEl);\n        }\n    }\n\n    onCloned({ cloneEl }) {\n        const tabsEls = getElementsWithOption(cloneEl, tabsSectionSelector);\n        for (const tabsEl of tabsEls) {\n            this.generateUniqueIDs(tabsEl);\n        }\n    }\n\n    generateUniqueIDs(editingElement) {\n        const navLinkEls = this.getNavLinkEls(editingElement);\n        const tabPaneEls = this.getPaneEls(editingElement);\n        for (let i = 0; i < navLinkEls.length; i++) {\n            const id = uniqueId(new Date().getTime() + \"_\");\n            const idLink = \"nav_tabs_link_\" + id;\n            const idContent = \"nav_tabs_content_\" + id;\n            const navLinkEl = navLinkEls[i];\n            navLinkEl.id = idLink;\n            navLinkEl.href = \"#\" + idContent;\n            navLinkEl.setAttribute(\"aria-controls\", idContent);\n            const tabPaneEl = tabPaneEls[i];\n            tabPaneEl.id = idContent;\n            tabPaneEl.setAttribute(\"aria-labelledby\", idLink);\n        }\n    }\n}\nregistry.category(\"website-plugins\").add(NavTabsOptionPlugin.id, NavTabsOptionPlugin);\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { SNIPPET_SPECIFIC_END } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\n/**\n * @typedef { Object } NavTabsStyleOptionShared\n * @property { NavTabsStyleOptionPlugin['isNavItem'] } isNavItem\n * @property { NavTabsStyleOptionPlugin['getActiveOverlayButtons'] } getActiveOverlayButtons\n * @property { NavTabsStyleOptionPlugin['moveNavItem'] } moveNavItem\n */\n\nexport class NavTabsStyleOption extends BaseOptionComponent {\n    static template = \"website.NavTabsStyleOption\";\n    static selector = \".s_tabs\";\n    static applyTo = \".s_tabs_main\";\n}\n\nexport class NavTabsImagesStyleOption extends BaseOptionComponent {\n    static template = \"website.NavTabsImagesStyleOption\";\n    static selector = \".s_tabs_images\";\n    static applyTo = \".s_tabs_main\";\n}\n\nclass NavTabsStyleOptionPlugin extends Plugin {\n    static id = \"navTabsOptionStyle\";\n    static shared = [\"isNavItem\", \"getActiveOverlayButtons\", \"moveNavItem\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(SNIPPET_SPECIFIC_END, NavTabsStyleOption),\n            withSequence(SNIPPET_SPECIFIC_END, NavTabsImagesStyleOption),\n        ],\n        builder_actions: {\n            SetStyleAction,\n            SetDirectionAction,\n        },\n        has_overlay_options: { hasOption: (el) => this.isNavItem(el) },\n        get_overlay_buttons: withSequence(0, {\n            getButtons: this.getActiveOverlayButtons.bind(this),\n        }),\n        is_unremovable_selector: \".nav-item\",\n        unsplittable_node_predicates: this.isUnsplittable,\n    };\n\n    setup() {\n        this.isEditableRTL = this.config.isEditableRTL;\n        this.isBackendRTL = localization.direction === \"rtl\";\n    }\n\n    isNavItem(el) {\n        return el.matches(\".nav-item\") && !!el.closest(\".s_tabs, .s_tabs_images\");\n    }\n\n    getActiveOverlayButtons(target) {\n        if (!this.isNavItem(target)) {\n            this.overlayTarget = null;\n            return [];\n        }\n\n        this.overlayTarget = target;\n        const buttons = [];\n        const reverseButtons = this.isEditableRTL !== this.isBackendRTL;\n        const parentStyle = window.getComputedStyle(this.overlayTarget.parentElement);\n        const isVertical = parentStyle.flexDirection === \"column\";\n        const previousNavItemEl = this.overlayTarget.previousElementSibling;\n        const nextNavItemEl = this.overlayTarget.nextElementSibling;\n\n        if (previousNavItemEl) {\n            const direction = isVertical ? \"up\" : reverseButtons ? \"right\" : \"left\";\n            buttons.push({\n                class: `fa fa-fw fa-angle-${direction}`,\n                title: isVertical\n                    ? _t(\"Move up\")\n                    : this.isEditableRTL\n                    ? _t(\"Move right\")\n                    : _t(\"Move left\"),\n                handler: this.moveNavItem.bind(this, \"prev\"),\n            });\n        }\n\n        if (nextNavItemEl) {\n            const direction = isVertical ? \"down\" : reverseButtons ? \"left\" : \"right\";\n            buttons.push({\n                class: `fa fa-fw fa-angle-${direction}`,\n                title: isVertical\n                    ? _t(\"Move down\")\n                    : this.isEditableRTL\n                    ? _t(\"Move left\")\n                    : _t(\"Move right\"),\n                handler: this.moveNavItem.bind(this, \"next\"),\n            });\n        }\n\n        if (reverseButtons && !isVertical) {\n            buttons.reverse();\n        }\n\n        return buttons;\n    }\n\n    moveNavItem(direction) {\n        const tabHash = this.overlayTarget.querySelector(\".nav-link\").hash;\n        const tabPaneEl = this.overlayTarget.closest(\"section\").querySelector(tabHash);\n\n        if (direction === \"prev\") {\n            const previousNavItemEl = this.overlayTarget.previousElementSibling;\n            const previousTabPaneEl = tabPaneEl.previousElementSibling;\n            previousNavItemEl.before(this.overlayTarget);\n            previousTabPaneEl.before(tabPaneEl);\n        } else {\n            const nextNavItemEl = this.overlayTarget.nextElementSibling;\n            const nextTabPaneEl = tabPaneEl.nextElementSibling;\n            nextNavItemEl.after(this.overlayTarget);\n            nextTabPaneEl.after(tabPaneEl);\n        }\n    }\n\n    isUnsplittable(node) {\n        return (\n            node &&\n            node.nodeType === Node.ELEMENT_NODE &&\n            node.closest(\".s_tabs, .s_tabs_images\") &&\n            node.closest(\"li\")?.classList.contains(\"nav-item\")\n        );\n    }\n}\n\nconst getTabsEl = (editingElement) => editingElement.querySelector(\".s_tabs_nav\");\n\nexport class BaseNavtabsStyleOption extends BuilderAction {\n    static id = \"baseNavtabsStyle\";\n    static dependencies = [\"navTabsOptionStyle\"];\n    setup() {\n        this.tabsTabsClasses = [\n            \"card-header\",\n            \"px-0\",\n            \"border-0\",\n            \"overflow-x-auto\",\n            \"overflow-y-hidden\",\n        ];\n        this.navTabsClasses = [\"card-header-tabs\", \"mx-0\", \"px-2\", \"border-bottom\"];\n        this.tabsBtnClasses = [\"d-flex\", \"rounded\"];\n        this.navBtnClasses = [\"d-inline-flex\", \"nav-pills\", \"p-2\"];\n\n        this.overlayTarget = null;\n    }\n\n    moveNavItem(direction) {\n        this.dependencies.navTabsOptionStyle.moveNavItem(direction);\n    }\n    getActiveOverlayButtons(target) {\n        return this.dependencies.navTabsOptionStyle.getActiveOverlayButtons(target);\n    }\n    isNavItem(el) {\n        return this.dependencies.navTabsOptionStyle.isNavItem(el);\n    }\n    getNavEl(editingElement) {\n        return editingElement.querySelector(\".s_tabs_nav .nav\");\n    }\n\n    applyDirection(editingElement, direction) {\n        // s_tabs_images use flex-md classes, while s_tabs use flex-sm classes\n        const isTabsImages = editingElement\n            .closest(\".s_tabs_common\")\n            .classList.contains(\"s_tabs_images\");\n\n        const isVertical = direction === \"vertical\";\n        const navEl = this.getNavEl(editingElement);\n\n        editingElement.classList.toggle(\"row\", isVertical);\n        editingElement.classList.toggle(\"s_col_no_resize\", isVertical);\n        editingElement.classList.toggle(\"s_col_no_bgcolor\", isVertical);\n        navEl.classList.toggle(isTabsImages ? \"flex-md-column\" : \"flex-sm-column\", isVertical);\n        editingElement\n            .querySelectorAll(\".s_tabs_nav > .nav-link\")\n            .forEach((linkEl) => linkEl.classList.toggle(\"py-2\", isVertical));\n        editingElement\n            .querySelector(\".s_tabs_nav\")\n            .classList.toggle(isTabsImages ? \"col-md-3\" : \"col-sm-3\", isVertical);\n        editingElement\n            .querySelector(\".s_tabs_content\")\n            .classList.toggle(isTabsImages ? \"col-md-9\" : \"col-sm-9\", isVertical);\n\n        // Clean incompatible leftover classes in vertical mode.\n        // See \"Fill and Justify\" and \"Alignment\" options.\n        if (isVertical) {\n            navEl.classList.remove(\n                \"nav-fill\",\n                \"nav-justified\",\n                \"justify-content-center\",\n                \"justify-content-end\",\n                \"ms-auto\",\n                \"mx-auto\"\n            );\n        }\n    }\n}\n\nclass SetStyleAction extends BaseNavtabsStyleOption {\n    static id = \"setStyle\";\n    isApplied({ editingElement, value }) {\n        const navEl = this.getNavEl(editingElement);\n        // 'nav-buttons' also applies 'nav-pills'\n        if (navEl.classList.contains(\"nav-buttons\")) {\n            return value === \"nav-buttons\";\n        }\n        return navEl.classList.contains(value);\n    }\n    apply({ editingElement, value }) {\n        const isTabs = value === \"nav-tabs\";\n        const isBtns = value === \"nav-buttons\";\n        const tabsEl = getTabsEl(editingElement);\n        const navEl = this.getNavEl(editingElement);\n\n        if (isTabs || isBtns) {\n            this.applyDirection(editingElement, \"horizontal\");\n        }\n\n        if (isTabs) {\n            tabsEl.classList.add(...this.tabsTabsClasses);\n            navEl.classList.add(...this.navTabsClasses);\n        } else if (isBtns) {\n            tabsEl.classList.add(...this.tabsBtnClasses);\n            navEl.classList.add(...this.navBtnClasses);\n        }\n        navEl.classList.add(value);\n\n        editingElement.classList.toggle(\"card\", isTabs);\n        tabsEl.classList.toggle(\"mb-3\", !isTabs);\n        navEl.classList.toggle(\"overflow-x-auto\", !isTabs);\n        navEl.classList.toggle(\"overflow-y-hidden\", !isTabs);\n        editingElement.querySelector(\".s_tabs_content\").classList.toggle(\"p-3\", isTabs);\n    }\n    clean({ editingElement, value }) {\n        const isTabs = value === \"nav-tabs\";\n        const isBtns = value === \"nav-buttons\";\n        const tabsEl = getTabsEl(editingElement);\n        const navEl = this.getNavEl(editingElement);\n\n        if (isTabs) {\n            tabsEl.classList.remove(...this.tabsTabsClasses);\n            navEl.classList.remove(...this.navTabsClasses);\n        } else if (isBtns) {\n            tabsEl.classList.remove(...this.tabsBtnClasses);\n            navEl.classList.remove(...this.navBtnClasses);\n        }\n        navEl.classList.remove(value);\n    }\n}\nclass SetDirectionAction extends BaseNavtabsStyleOption {\n    static id = \"setDirection\";\n    isApplied({ editingElement, value }) {\n        const classList = this.getNavEl(editingElement).classList;\n        const containsFlexColumn =\n            classList.contains(\"flex-sm-column\") || classList.contains(\"flex-md-column\");\n        return value === \"vertical\" ? containsFlexColumn : !containsFlexColumn;\n    }\n    apply({ editingElement, value }) {\n        this.applyDirection(editingElement, value);\n    }\n}\n\nregistry.category(\"website-plugins\").add(NavTabsStyleOptionPlugin.id, NavTabsStyleOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nexport class ParallaxOption extends BaseOptionComponent {\n    static template = \"website.ParallaxOption\";\n}\n", "import { applyFunDependOnSelectorAndExclude } from \"@html_builder/plugins/utils\";\nimport { filterExtends } from \"@html_builder/utils/utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { BaseWebsiteBackgroundOption } from \"./background_option\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\n/**\n * @typedef { Object } WebsiteParallaxShared\n * @property { WebsiteParallaxPlugin['applyParallaxType'] } applyParallaxType\n */\n\nclass WebsiteParallaxPlugin extends Plugin {\n    static id = \"websiteParallaxPlugin\";\n    static dependencies = [\"builderActions\", \"backgroundImageOption\"];\n    static shared = [\"applyParallaxType\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_actions: {\n            SetParallaxTypeAction,\n        },\n        on_bg_image_hide_handlers: this.onBgImageHide.bind(this),\n        content_not_editable_selectors: \".s_parallax_bg, section.s_parallax > .oe_structure\",\n        system_node_selectors: \".s_parallax_bg\",\n        get_target_element_providers: withSequence(1, this.getTargetElement),\n    };\n    setup() {\n        this.backgroundOptionClasses = filterExtends(\n            this.getResource(\"builder_options\"),\n            BaseWebsiteBackgroundOption\n        );\n    }\n    applyParallaxType({ editingElement, value }) {\n        const isParallax = value !== \"none\";\n        editingElement.classList.toggle(\"parallax\", isParallax);\n        editingElement.classList.toggle(\"s_parallax_is_fixed\", value === \"fixed\");\n        editingElement.classList.toggle(\n            \"s_parallax_no_overflow_hidden\",\n            value === \"none\" || value === \"fixed\"\n        );\n        const typeValues = {\n            none: 0,\n            fixed: 1,\n            top: 1.5,\n            bottom: -1.5,\n            zoomIn: 0.2,\n            zoomOut: 1.2,\n        };\n        editingElement.dataset.scrollBackgroundRatio = typeValues[value];\n        // Set a parallax type only if there is a zoom option selected.\n        // This is to avoid useless element in the DOM since in the animation\n        // we need the type only for zoom options.\n        if (value === \"zoomIn\" || value === \"zoomOut\") {\n            editingElement.dataset.parallaxType = value;\n        } else {\n            delete editingElement.dataset.parallaxType;\n        }\n        let parallaxEl = editingElement.querySelector(\":scope > .s_parallax_bg\");\n        if (isParallax) {\n            if (!parallaxEl) {\n                parallaxEl = document.createElement(\"span\");\n                parallaxEl.classList.add(\"s_parallax_bg\");\n                editingElement.prepend(parallaxEl);\n                this.dependencies.backgroundImageOption.changeEditingEl(editingElement, parallaxEl);\n            }\n        } else if (parallaxEl) {\n            this.dependencies.backgroundImageOption.changeEditingEl(parallaxEl, editingElement);\n            parallaxEl.remove();\n        }\n    }\n    onBgImageHide(rootEl) {\n        for (const backgroundClass of this.backgroundOptionClasses) {\n            applyFunDependOnSelectorAndExclude(\n                this.removeParallax.bind(this),\n                rootEl,\n                backgroundClass\n            );\n        }\n    }\n    removeParallax(editingEl) {\n        const parallaxEl = editingEl.querySelector(\":scope > .s_parallax_bg\");\n        const bgImage = parallaxEl?.style.backgroundImage;\n        if (\n            !parallaxEl ||\n            !bgImage ||\n            bgImage === \"none\" ||\n            editingEl.classList.contains(\"o_background_video\")\n        ) {\n            // The parallax option was enabled but the background image was\n            // removed or a background video has been added: disable the\n            // parallax option.\n            this.applyParallaxType({\n                editingElement: editingEl,\n                value: \"none\",\n            });\n        }\n    }\n    getTargetElement(editingEl) {\n        return editingEl.classList.contains(\"s_parallax_bg\") ? editingEl.parentElement : editingEl;\n    }\n}\nexport class SetParallaxTypeAction extends BuilderAction {\n    static id = \"setParallaxType\";\n    static dependencies = [\"websiteParallaxPlugin\"];\n    apply(context) {\n        this.dependencies.websiteParallaxPlugin.applyParallaxType(context);\n    }\n    isApplied({ editingElement, value }) {\n        const attributeValue = parseFloat(\n            editingElement.dataset.scrollBackgroundRatio?.trim() || 0\n        );\n        if (attributeValue === 0) {\n            return value === \"none\";\n        }\n        if (attributeValue === 1) {\n            return value === \"fixed\";\n        }\n        const parallaxType = editingElement.dataset.parallaxType;\n        if (parallaxType) {\n            // Compatibility: Previously, \"zoom_out\" and \"zoom_in\" had their\n            // behavior reversed. The previous \"zoom_out\" correspond to the\n            // current \"zoomIn\" type.\n            if (parallaxType === \"zoom_out\") {\n                return value === \"zoomIn\";\n            }\n            if (parallaxType === \"zoom_in\") {\n                return value === \"zoomOut\";\n            }\n            return value === parallaxType;\n        }\n        return attributeValue > 0 ? value === \"top\" : value === \"bottom\";\n    }\n}\n\nregistry.category(\"website-plugins\").add(WebsiteParallaxPlugin.id, WebsiteParallaxPlugin);\n", "import { SNIPPET_SPECIFIC, SNIPPET_SPECIFIC_END } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport const POPUP = SNIPPET_SPECIFIC;\nexport const COOKIES_BAR = SNIPPET_SPECIFIC_END;\n\nexport class PopupOption extends BaseOptionComponent {\n    static template = \"website.PopupOption\";\n    static selector = \".s_popup\";\n    static exclude = \"#website_cookies_bar\";\n    static applyTo = \".modal\";\n}\n\nexport class PopupCookiesOption extends BaseOptionComponent {\n    static template = \"website.PopupCookiesOption\";\n    static selector = \".s_popup#website_cookies_bar\";\n    static applyTo = \".modal\";\n}\n\nclass PopupOptionPlugin extends Plugin {\n    static id = \"PopupOption\";\n    static dependencies = [\"anchor\", \"visibility\", \"history\", \"popupVisibilityPlugin\"];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(POPUP, PopupOption),\n            withSequence(COOKIES_BAR, PopupCookiesOption),\n        ],\n        dropzone_selector: {\n            selector: \".s_popup\",\n            exclude: \"#website_cookies_bar\",\n            dropIn: \":not(p).oe_structure:not(.oe_structure_solo):not([data-snippet] *), :not(.o_mega_menu):not(p)[data-oe-type=html]:not([data-snippet] *)\",\n        },\n        builder_actions: {\n            // Moves the snippet in #o_shared_blocks to be common to all pages\n            // or inside the first editable oe_structure in the main to be on\n            // current page only.\n            MoveBlockAction,\n            SetBackdropAction,\n            CopyAnchorAction,\n            SetPopupDelayAction,\n        },\n        on_cloned_handlers: this.onCloned.bind(this),\n        on_snippet_dropped_handlers: this.onSnippetDropped.bind(this),\n        on_will_remove_handlers: this.onWillRemove.bind(this),\n        no_parent_containers: \".s_popup\",\n    };\n\n    onCloned({ cloneEl }) {\n        if (cloneEl.matches(\".s_popup\")) {\n            this.assignUniqueID(cloneEl);\n        }\n    }\n\n    onSnippetDropped({ snippetEl }) {\n        if (snippetEl.matches(\".s_popup\")) {\n            this.assignUniqueID(snippetEl);\n            this.dependencies.history.addCustomMutation({\n                apply: () => {\n                    this.dependencies.visibility.toggleTargetVisibility(snippetEl, true);\n                },\n                revert: () => {\n                    this.dependencies.visibility.toggleTargetVisibility(snippetEl, false);\n                },\n            });\n        }\n    }\n\n    onWillRemove(el) {\n        this.dependencies.visibility.toggleTargetVisibility(el, false);\n        this.dependencies.history.addCustomMutation({\n            apply: () => {\n                this.dependencies.visibility.toggleTargetVisibility(el, false);\n            },\n            revert: () => {\n                this.dependencies.visibility.toggleTargetVisibility(el, true);\n            },\n        });\n    }\n\n    assignUniqueID(editingElement) {\n        editingElement.closest(\".s_popup\").id = `sPopup${Date.now()}`;\n    }\n}\n\n// Moves the snippet in #o_shared_blocks to be common to all pages\n// or inside the first editable oe_structure in the main to be on\n// current page only.\nexport class MoveBlockAction extends BuilderAction {\n    static id = \"moveBlock\";\n    isApplied({ editingElement, value }) {\n        return editingElement.closest(\"#o_shared_blocks\")\n            ? value === \"allPages\"\n            : value === \"currentPage\";\n    }\n    apply({ editingElement, value }) {\n        const selector =\n            value === \"allPages\" ? \"#o_shared_blocks\" : \"main .oe_structure.o_editable\";\n        const whereEl = this.editable.querySelector(selector);\n        const popupEl = editingElement.closest(\".s_popup\");\n        whereEl.insertAdjacentElement(\"afterbegin\", popupEl);\n    }\n}\nexport class SetBackdropAction extends BuilderAction {\n    static id = \"setBackdrop\";\n    isApplied({ editingElement }) {\n        const hasBackdropColor = !!editingElement.style.getPropertyValue(\"background-color\").trim();\n        const hasNoBackdropClass = editingElement.classList.contains(\"s_popup_no_backdrop\");\n        return hasBackdropColor && !hasNoBackdropClass;\n    }\n    apply({ editingElement }) {\n        editingElement.classList.remove(\"s_popup_no_backdrop\");\n        editingElement.style.setProperty(\"background-color\", \"var(--black-50)\", \"important\");\n    }\n    clean({ editingElement }) {\n        editingElement.classList.add(\"s_popup_no_backdrop\");\n        editingElement.style.removeProperty(\"background-color\");\n    }\n}\nexport class CopyAnchorAction extends BuilderAction {\n    static id = \"copyAnchor\";\n    static dependencies = [\"anchor\"];\n    apply({ editingElement }) {\n        this.dependencies.anchor.createOrEditAnchorLink(editingElement);\n    }\n}\nexport class SetPopupDelayAction extends BuilderAction {\n    static id = \"setPopupDelay\";\n    apply({ editingElement, value }) {\n        editingElement.dataset.showAfter = value * 1000;\n    }\n    getValue({ editingElement }) {\n        return editingElement.dataset.showAfter / 1000;\n    }\n}\n\nregistry.category(\"website-plugins\").add(PopupOptionPlugin.id, PopupOptionPlugin);\n", "import { BEGIN, SNIPPET_SPECIFIC_END } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { isElement } from \"@html_editor/utils/dom_info\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\nimport { BaseAddProductOption } from \"@html_builder/plugins/add_product_option\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { BorderConfigurator } from \"@html_builder/plugins/border_configurator_option\";\n\nexport class PriceListBoxedDescriptionOption extends BaseOptionComponent {\n    static template = \"website.PriceListBoxedDescriptionOption\";\n    static selector = \".s_pricelist_boxed\";\n    static components = { BorderConfigurator };\n}\n\nexport class AddProductPricelistBoxedOption extends BaseAddProductOption {\n    static selector = \".s_pricelist_boxed\";\n    buttonApplyTo =\n        \":scope > :has(.s_pricelist_boxed_item):not(:has(.row > div .s_pricelist_boxed_item))\";\n    productSelector = \".s_pricelist_boxed_item\";\n}\n\nexport class AddProductPricelistBoxedSectionOption extends BaseAddProductOption {\n    static selector = \".s_pricelist_boxed_section\";\n    buttonApplyTo = \":scope > :has(.s_pricelist_boxed_item)\";\n    productSelector = \".s_pricelist_boxed_item\";\n}\n\nclass PriceListBoxedOptionPlugin extends Plugin {\n    static id = \"priceListBoxedOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(BEGIN, AddProductPricelistBoxedOption),\n            withSequence(BEGIN, AddProductPricelistBoxedSectionOption),\n            withSequence(SNIPPET_SPECIFIC_END, PriceListBoxedDescriptionOption),\n        ],\n        dropzone_selector: {\n            selector: \".s_pricelist_boxed_item\",\n            dropNear: \".s_pricelist_boxed_item\",\n        },\n        is_movable_selector: { selector: \".s_pricelist_boxed_item\", direction: \"vertical\" },\n        // Protect pricelist item, price, and description blocks from being\n        // split/merged by the delete plugin.\n        unsplittable_node_predicates: (node) =>\n            isElement(node) &&\n            node.matches(\n                \".s_pricelist_boxed_item, .s_pricelist_boxed_item_price, .s_pricelist_boxed_item_description\"\n            ),\n    };\n}\n\nregistry.category(\"website-plugins\").add(PriceListBoxedOptionPlugin.id, PriceListBoxedOptionPlugin);\n", "import { BEGIN, SNIPPET_SPECIFIC_END } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { isElement } from \"@html_editor/utils/dom_info\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\nimport { BaseAddProductOption } from \"@html_builder/plugins/add_product_option\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { BaseVerticalAlignmentOption } from \"@html_builder/plugins/base_vertical_alignment_option\";\nimport { BorderConfigurator } from \"@html_builder/plugins/border_configurator_option\";\n\nexport class PriceListCafeDescriptionOption extends BaseOptionComponent {\n    static template = \"website.PriceListCafeDescriptionOption\";\n    static selector = \".s_pricelist_cafe\";\n    static components = { BorderConfigurator };\n}\n\nexport class PricelistCafeVerticalAlignmentOption extends BaseVerticalAlignmentOption {\n    static selector = \".s_pricelist_cafe\";\n    static applyTo = \".row:has(.s_pricelist_cafe_col)\";\n    level = 0;\n}\n\nexport class AddProductPricelistCafeOption extends BaseAddProductOption {\n    static selector = \".s_pricelist_cafe\";\n    buttonApplyTo =\n        \":scope > :has(.s_pricelist_cafe_item):not(:has(.row > div .s_pricelist_cafe_item))\";\n    productSelector = \".s_pricelist_cafe_item\";\n}\n\nexport class AddProductPricelistCafeRowOption extends BaseAddProductOption {\n    static selector = \".s_pricelist_cafe .row > div\";\n    buttonApplyTo = \":scope > :has(.s_pricelist_cafe_item)\";\n    productSelector = \".s_pricelist_cafe_item\";\n}\n\nclass PriceListCafePlugin extends Plugin {\n    static id = \"priceList\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(BEGIN, AddProductPricelistCafeOption),\n            withSequence(BEGIN, PricelistCafeVerticalAlignmentOption),\n            withSequence(BEGIN, AddProductPricelistCafeRowOption),\n            withSequence(SNIPPET_SPECIFIC_END, PriceListCafeDescriptionOption),\n        ],\n        dropzone_selector: {\n            selector: \".s_pricelist_cafe_item\",\n            dropNear: \".s_pricelist_cafe_item\",\n        },\n        is_movable_selector: { selector: \".s_pricelist_cafe_item\", direction: \"vertical\" },\n        // Protect pricelist item, price, and description blocks from being\n        // split/merged by the delete plugin.\n        unsplittable_node_predicates: (node) =>\n            isElement(node) &&\n            node.matches(\n                \".s_pricelist_cafe_item, .s_pricelist_cafe_item_price, .s_pricelist_cafe_item_description\"\n            ),\n    };\n}\n\nregistry.category(\"website-plugins\").add(PriceListCafePlugin.id, PriceListCafePlugin);\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nclass PriceListPlugin extends Plugin {\n    static id = \"priceListPlugin\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_actions: {\n            TogglePriceListDescriptionAction,\n        },\n    };\n}\n\nexport class TogglePriceListDescriptionAction extends BuilderAction {\n    static id = \"togglePriceListDescription\";\n    isApplied({ editingElement, params }) {\n        const description = editingElement.querySelector(`.${params.descriptionClass}`);\n        return description && !description.classList.contains(\"d-none\");\n    }\n    apply({ editingElement, params }) {\n        const items = editingElement.querySelectorAll(`.${params.itemClass}`);\n        for (const item of items) {\n            const description = item.querySelector(\".\" + params.descriptionClass);\n            if (description) {\n                description.classList.remove(\"d-none\");\n            } else {\n                const descriptionEl = this.document.createElement(\"p\");\n                descriptionEl.classList.add(\n                    params.descriptionClass,\n                    \"d-block\",\n                    \"mt-2\",\n                    \"pe-5\",\n                    \"text-muted\"\n                );\n                if (params.descriptionExtraClass) {\n                    descriptionEl.classList.add(params.descriptionExtraClass);\n                }\n                descriptionEl.textContent = _t(\"Add a description here\");\n                item.appendChild(descriptionEl);\n            }\n        }\n    }\n    clean({ editingElement, params }) {\n        const items = editingElement.querySelectorAll(`.${params.itemClass}`);\n        for (const item of items) {\n            const description = item.querySelector(\".\" + params.descriptionClass);\n            if (description) {\n                description.classList.add(\"d-none\");\n            }\n        }\n    }\n}\n\nregistry.category(\"website-plugins\").add(PriceListPlugin.id, PriceListPlugin);\n", "import { BEGIN, SNIPPET_SPECIFIC_END } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { isElement } from \"@html_editor/utils/dom_info\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\nimport { BaseAddProductOption } from \"@html_builder/plugins/add_product_option\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { BorderConfigurator } from \"@html_builder/plugins/border_configurator_option\";\n\nexport class ProductCatalogDescriptionOption extends BaseOptionComponent {\n    static template = \"website.ProductCatalogDescriptionOption\";\n    static selector = \".s_product_catalog\";\n    static components = { BorderConfigurator };\n}\n\nexport class AddProductCatalogOption extends BaseAddProductOption {\n    static selector = \".s_product_catalog\";\n    buttonApplyTo =\n        \":scope > :has(.s_product_catalog_dish):not(:has(.row > div .s_product_catalog_dish))\";\n    productSelector = \".s_product_catalog_dish\";\n}\n\nexport class AddProductCatalogSectionOption extends BaseAddProductOption {\n    static selector = \".s_product_catalog .row > div\";\n    buttonApplyTo = \":scope > :has(.s_product_catalog_dish)\";\n    productSelector = \".s_product_catalog_dish\";\n}\n\nclass ProductCatalogOptionPlugin extends Plugin {\n    static id = \"productCatalogOptionPlugin\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(BEGIN, AddProductCatalogOption),\n            withSequence(BEGIN, AddProductCatalogSectionOption),\n            withSequence(SNIPPET_SPECIFIC_END, ProductCatalogDescriptionOption),\n        ],\n        dropzone_selector: {\n            selector: \".s_product_catalog_dish\",\n            dropNear: \".s_product_catalog_dish\",\n        },\n        is_movable_selector: { selector: \".s_product_catalog_dish\", direction: \"vertical\" },\n        // Protect pricelist item, price, and description blocks from being\n        // split/merged by the delete plugin.\n        unsplittable_node_predicates: (node) =>\n            isElement(node) &&\n            node.matches(\n                \".s_product_catalog_dish, .s_product_catalog_dish_price, .s_product_catalog_dish_description\"\n            ),\n    };\n}\n\nregistry.category(\"website-plugins\").add(ProductCatalogOptionPlugin.id, ProductCatalogOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport const connectorOptionParams = [\n    { key: \"\", param: \"None\" },\n    { key: \"s_process_steps_connector_line\", param: \"Line\" },\n    { key: \"s_process_steps_connector_arrow\", param: \"Straight arrow\" },\n    { key: \"s_process_steps_connector_curved_arrow\", param: \"Curved arrow\" },\n];\n\nexport class ProcessStepsOption extends BaseOptionComponent {\n    static template = \"website.ProcessStepsOption\";\n    static selector = \".s_process_steps\";\n\n    setup() {\n        super.setup();\n        this.connectorOptionParams = connectorOptionParams;\n    }\n\n    getConnectorId(connectorOptionParamKey) {\n        return !connectorOptionParamKey ? \"no_connector_opt\" : \"\";\n    }\n}\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { ClassAction } from \"@html_builder/core/core_builder_action_plugin\";\nimport { applyFunDependOnSelectorAndExclude } from \"@html_builder/plugins/utils\";\nimport { after } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\nimport { CONTAINER_WIDTH } from \"@website/builder/option_sequence\";\nimport { connectorOptionParams, ProcessStepsOption } from \"./process_steps_option\";\nimport { BaseWebsiteBackgroundOption } from \"./background_option\";\n\nexport class WebsiteBackgroundProcessStepOption extends BaseWebsiteBackgroundOption {\n    static selector = \".s_process_step .s_process_step_number\";\n    static defaultProps = {\n        withColors: true,\n        withImages: false,\n        withColorCombinations: false,\n    };\n}\n\nclass ProcessStepsOptionPlugin extends Plugin {\n    static id = \"processStepsOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(after(CONTAINER_WIDTH), ProcessStepsOption),\n            WebsiteBackgroundProcessStepOption,\n        ],\n        builder_actions: {\n            ChangeConnectorAction,\n            ChangeArrowColorAction,\n        },\n        // The reload of the connectors is done at the\n        // 'content_updated_handlers' (each time there is a DOM mutation) and\n        // not at the normalize as there are cases where we want to reload the\n        // connectors even if there were no step added (e.g: a column of the\n        // snippet is being resized).\n        content_updated_handlers: (rootEl) =>\n            applyFunDependOnSelectorAndExclude(reloadConnectors, rootEl, {\n                selector: ProcessStepsOption.selector,\n            }),\n        dropzone_selector: {\n            selector: \".s_process_step\",\n            dropLockWithin: \".s_process_steps\",\n        },\n    };\n}\n\nexport class ChangeConnectorAction extends ClassAction {\n    static id = \"changeConnector\";\n    apply({ editingElement, params: { mainParam: className } }) {\n        super.apply(...arguments);\n        reloadConnectors(editingElement);\n        let markerEnd = \"\";\n        if (\n            [\"s_process_steps_connector_arrow\", \"s_process_steps_connector_curved_arrow\"].includes(\n                className\n            )\n        ) {\n            const arrowHeadEl = editingElement.querySelector(\".s_process_steps_arrow_head\");\n            // The arrowhead id is set here so that they are different per snippet\n            if (!arrowHeadEl.id) {\n                arrowHeadEl.id = \"s_process_steps_arrow_head\" + Date.now();\n            }\n            markerEnd = `url(#${arrowHeadEl.id})`;\n        }\n        editingElement\n            .querySelectorAll(\".s_process_step_connector path\")\n            .forEach((path) => path.setAttribute(\"marker-end\", markerEnd));\n    }\n}\n\nexport class ChangeArrowColorAction extends BuilderAction {\n    static id = \"changeArrowColor\";\n    apply({ editingElement, value: colorValue }) {\n        const arrowHeadEl = editingElement\n            .closest(\".s_process_steps\")\n            .querySelector(\".s_process_steps_arrow_head\");\n        arrowHeadEl.querySelector(\"path\").style.fill = colorValue;\n    }\n}\n\nregistry.category(\"website-plugins\").add(ProcessStepsOptionPlugin.id, ProcessStepsOptionPlugin);\n\n/**\n * Width and position of the connectors should be updated when one of the\n * steps is modified.\n *\n */\nfunction reloadConnectors(editingElement) {\n    const connectorOptionClasses = connectorOptionParams.map(\n        (connectorOptionParam) => connectorOptionParam.key\n    );\n    const type =\n        connectorOptionClasses.find(\n            (connectorOptionClass) =>\n                connectorOptionClass && editingElement.classList.contains(connectorOptionClass)\n        ) || \"\";\n    // As the connectors are only visible in desktop, we can ignore the\n    // steps that are only visible in mobile.\n    const stepsEls = editingElement.querySelectorAll(\n        \".s_process_step:not(.o_snippet_desktop_invisible)\"\n    );\n    const nbBootstrapCols = 12;\n    let colsInRow = 0;\n\n    for (let i = 0; i < stepsEls.length - 1; i++) {\n        const connectorEl = stepsEls[i].querySelector(\".s_process_step_connector\");\n        const stepMainElementRect = getStepMainElementRect(stepsEls[i]);\n        const nextStepMainElementRect = getStepMainElementRect(stepsEls[i + 1]);\n        const stepSize = getClassSuffixedInteger(stepsEls[i], \"col-lg-\");\n        const nextStepSize = getClassSuffixedInteger(stepsEls[i + 1], \"col-lg-\");\n        const stepOffset = getClassSuffixedInteger(stepsEls[i], \"offset-lg-\");\n        const nextStepOffset = getClassSuffixedInteger(stepsEls[i + 1], \"offset-lg-\");\n        const stepPaddingTop = getClassSuffixedInteger(stepsEls[i], \"pt\");\n        const nextStepPaddingTop = getClassSuffixedInteger(stepsEls[i + 1], \"pt\");\n        const stepHeightDifference = stepPaddingTop - nextStepPaddingTop;\n        const hCurrentStepIconHeight = stepMainElementRect.height / 2;\n        const hNextStepIconHeight = nextStepMainElementRect.height / 2;\n\n        connectorEl.style.left = `calc(50% + ${stepMainElementRect.width / 2}px + 16px)`;\n        connectorEl.style.height = `${\n            stepMainElementRect.height + Math.abs(stepHeightDifference)\n        }px`;\n        connectorEl.style.width = `calc(${\n            (100 * (stepSize / 2 + nextStepOffset + nextStepSize / 2)) / stepSize\n        }% - ${stepMainElementRect.width / 2}px - ${nextStepMainElementRect.width / 2}px - 32px)`;\n\n        const marginType = stepHeightDifference < 0 ? \"marginBottom\" : \"marginTop\";\n        connectorEl.style[marginType] = `${0 - Math.abs(stepHeightDifference)}px`;\n\n        const isTheLastColOfRow =\n            nbBootstrapCols < colsInRow + stepSize + stepOffset + nextStepSize + nextStepOffset;\n        connectorEl.classList.toggle(\"d-none\", isTheLastColOfRow);\n        colsInRow = isTheLastColOfRow ? 0 : colsInRow + stepSize + stepOffset;\n        // When we are mobile view, the connector is not visible, here we\n        // display it quickly just to have its size.\n        connectorEl.style.display = \"block\";\n        const { height, width } = connectorEl.getBoundingClientRect();\n        connectorEl.style.removeProperty(\"display\");\n        if (type === \"s_process_steps_connector_curved_arrow\" && i % 2 === 0) {\n            connectorEl.style.transform = stepHeightDifference ? \"unset\" : \"scale(1, -1)\";\n        } else {\n            connectorEl.style.transform = \"unset\";\n        }\n        connectorEl.setAttribute(\"viewBox\", `0 0 ${width} ${height}`);\n        connectorEl\n            .querySelector(\"path\")\n            .setAttribute(\n                \"d\",\n                getPath(\n                    type,\n                    width,\n                    height,\n                    stepHeightDifference,\n                    hCurrentStepIconHeight,\n                    hNextStepIconHeight\n                )\n            );\n    }\n}\n/**\n * Returns the number suffixed to the class given in parameter.\n *\n * @param {HTMLElement} el\n * @param {String} classNamePrefix\n * @returns {Integer}\n */\nfunction getClassSuffixedInteger(el, classNamePrefix) {\n    const className = [...el.classList].find((cl) => cl.startsWith(classNamePrefix));\n    return className ? parseInt(className.replace(classNamePrefix, \"\")) : 0;\n}\n/**\n * Returns the step's icon or content bounding rectangle.\n *\n * @param {HTMLElement}\n * @returns {object}\n */\nfunction getStepMainElementRect(stepEl) {\n    const iconEl = stepEl.querySelector(\".s_process_step_number\");\n    if (iconEl) {\n        return iconEl.getBoundingClientRect();\n    }\n    const contentEls = stepEl.querySelectorAll(\".s_process_step_content > *\");\n    // If there is no icon, the biggest text bloc in the content container\n    // will be chosen.\n    if (contentEls.length) {\n        const contentRects = [...contentEls].map((contentEl) => {\n            const range = document.createRange();\n            range.selectNodeContents(contentEl);\n            return range.getBoundingClientRect();\n        });\n        return contentRects.reduce((previous, current) =>\n            current.width > previous.width ? current : previous\n        );\n    }\n    return {};\n}\n/**\n * Returns the svg path based on the type of connector.\n *\n * @param {string} type\n * @param {integer} width\n * @param {integer} height\n * @returns {string}\n */\nfunction getPath(\n    type,\n    width,\n    height,\n    stepHeightDifference,\n    hCurrentStepIconHeight,\n    hNextStepIconHeight\n) {\n    const hHeight = height / 2;\n    switch (type) {\n        case \"s_process_steps_connector_line\": {\n            const verticalPaddingFactor = Math.abs(stepHeightDifference) / 8;\n            if (stepHeightDifference >= 0) {\n                return `M 0 ${\n                    stepHeightDifference + hCurrentStepIconHeight - verticalPaddingFactor\n                } L ${width} ${hNextStepIconHeight + verticalPaddingFactor}`;\n            }\n            return `M 0 ${hCurrentStepIconHeight + verticalPaddingFactor} L ${width} ${\n                hNextStepIconHeight - stepHeightDifference - verticalPaddingFactor\n            }`;\n        }\n        case \"s_process_steps_connector_arrow\": {\n            // When someone plays with the y-axis, it adds the padding in\n            // multiple of 8px. so here we devide it by 8 to calculate the\n            // number of padding steps has been added.\n            const verticalPaddingFactor = (Math.abs(stepHeightDifference) / 8) * 1.5;\n            if (stepHeightDifference >= 0) {\n                return `M ${0.05 * width} ${\n                    stepHeightDifference + hCurrentStepIconHeight - verticalPaddingFactor\n                } L ${0.95 * width - 6} ${hNextStepIconHeight + verticalPaddingFactor}`;\n            }\n            return `M ${0.05 * width} ${hCurrentStepIconHeight + verticalPaddingFactor} L ${\n                0.95 * width - 6\n            } ${Math.abs(stepHeightDifference) + hNextStepIconHeight - verticalPaddingFactor}`;\n        }\n        case \"s_process_steps_connector_curved_arrow\": {\n            if (stepHeightDifference == 0) {\n                return `M ${0.05 * width} ${hHeight * 1.2} Q ${width / 2} ${hHeight * 1.8} ${\n                    0.95 * width - 6\n                } ${hHeight * 1.2}`;\n            } else if (stepHeightDifference > 0) {\n                return `M ${0.05 * width} ${stepHeightDifference + hCurrentStepIconHeight} Q ${\n                    width * 0.75\n                } ${height * 0.75} ${0.5 * width - 6} ${hHeight} T ${\n                    0.95 * width - 6\n                } ${hNextStepIconHeight}`;\n            }\n            return `M ${0.05 * width} ${hCurrentStepIconHeight} Q ${width * 0.75} ${\n                height * 0.005\n            } ${0.5 * width - 6} ${hHeight} T ${0.95 * width - 6} ${\n                Math.abs(stepHeightDifference) + hNextStepIconHeight\n            }`;\n        }\n    }\n    return \"\";\n}\n", "import { registry } from \"@web/core/registry\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { clamp } from \"@web/core/utils/numbers\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class ProgressBarOption extends BaseOptionComponent {\n    static template = \"website.ProgressBarOption\";\n    static selector = \".s_progress_bar\";\n\n    static cleanForSave(editingEl) {\n        const progressBar = editingEl.querySelector(\".progress-bar\");\n        const progressLabel = editingEl.querySelector(\".s_progress_bar_text\");\n\n        if (!progressBar.classList.contains(\"progress-bar-striped\")) {\n            progressBar.classList.remove(\"progress-bar-animated\");\n        }\n\n        if (progressLabel && progressLabel.classList.contains(\"d-none\")) {\n            progressLabel.remove();\n        }\n    }\n}\n\nclass ProgressBarOptionPlugin extends Plugin {\n    static id = \"progressBarOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: ProgressBarOption,\n        builder_actions: {\n            DisplayAction,\n            ProgressBarValueAction,\n        },\n        so_content_addition_selector: [\".s_progress_bar\"],\n    };\n}\n\nexport class DisplayAction extends BuilderAction {\n    static id = \"display\";\n    apply({ editingElement, params: { mainParam: position } }) {\n        // retro-compatibility\n        if (editingElement.classList.contains(\"progress\")) {\n            editingElement.classList.remove(\"progress\");\n            const progressBarEl = editingElement.querySelector(\".progress-bar\");\n            if (progressBarEl) {\n                const wrapperEl = document.createElement(\"div\");\n                wrapperEl.classList.add(\"progress\");\n                progressBarEl.parentNode.insertBefore(wrapperEl, progressBarEl);\n                wrapperEl.appendChild(progressBarEl);\n                editingElement\n                    .querySelector(\".progress-bar span\")\n                    .classList.add(\"s_progress_bar_text\");\n            }\n        }\n\n        const progress = editingElement.querySelector(\".progress\");\n        const progressValue = progress.getAttribute(\"aria-valuenow\");\n        let progressLabel = editingElement.querySelector(\".s_progress_bar_text\");\n\n        if (!progressLabel && position !== \"none\") {\n            progressLabel = document.createElement(\"span\");\n            progressLabel.classList.add(\"s_progress_bar_text\", \"small\");\n            progressLabel.textContent = progressValue + \"%\";\n        }\n\n        if (position === \"inline\") {\n            editingElement.querySelector(\".progress-bar\").appendChild(progressLabel);\n        } else if ([\"below\", \"after\"].includes(position)) {\n            progress.insertAdjacentElement(\"afterend\", progressLabel);\n        }\n\n        // Added to address the prior omission of s_progress_bar_text in s_numbers_charts\n        if (progressLabel) {\n            // Temporary hide the label. It's effectively removed in cleanForSave\n            // if the option is confirmed\n            progressLabel.classList.toggle(\"d-none\", position === \"none\");\n        }\n    }\n}\n\nexport class ProgressBarValueAction extends BuilderAction {\n    static id = \"progressBarValue\";\n    apply({ editingElement, value }) {\n        value = parseInt(value);\n        value = clamp(value, 0, 100);\n        const progressBarEl = editingElement.querySelector(\".progress-bar\");\n        const progressBarTextEl = editingElement.querySelector(\".s_progress_bar_text\");\n        const progressMainEl = editingElement.querySelector(\".progress\");\n        // Added to address the prior omission of s_progress_bar_text in s_numbers_charts\n        if (progressBarTextEl) {\n            // Target precisely the XX% not only XX to not replace wrong element\n            // eg 'Since 1978 we have completed 45%' <- don't replace 1978\n            progressBarTextEl.innerText = progressBarTextEl.innerText.replace(\n                /[0-9]+%/,\n                value + \"%\"\n            );\n        }\n        progressMainEl.setAttribute(\"aria-valuenow\", value);\n        progressBarEl.style.width = value + \"%\";\n    }\n    getValue({ editingElement }) {\n        return editingElement.querySelector(\".progress\").getAttribute(\"aria-valuenow\");\n    }\n}\n\nregistry.category(\"website-plugins\").add(ProgressBarOptionPlugin.id, ProgressBarOptionPlugin);\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ScrollButtonOption extends BaseOptionComponent {\n    static template = \"website.ScrollButtonOption\";\n    static selector = \"section\";\n    static exclude =\n        \"[data-snippet] :not(.oe_structure) > [data-snippet], .s_instagram_page, .o_mega_menu > section, .s_appointments .s_dynamic_snippet_content, .s_bento_banner section[data-name='Card'], .s_floating_blocks, .s_floating_blocks .s_floating_blocks_block, .s_bento_block_card, .s_dynamic_category, .s_dynamic_category .s_dynamic_snippet_title, .s_announcement_scroll\";\n\n    setup() {\n        super.setup();\n        this.state = useDomState((editingElement) => ({\n            heightLabel:\n                editingElement.dataset.snippet === \"s_image_gallery\"\n                    ? _t(\"Min-Height\")\n                    : _t(\"Height\"),\n            heightFieldEnabled: editingElement.dataset.snippet === \"s_image_gallery\",\n        }));\n    }\n\n    showHeightField() {\n        return this.state.heightFieldEnabled && this.isActiveItem(\"minheight_auto_opt\");\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { ScrollButtonOption } from \"./scroll_button_option\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { ClassAction } from \"@html_builder/core/core_builder_action_plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { SCROLL_BUTTON } from \"@website/builder/option_sequence\";\n\nclass ScrollButtonOptionPlugin extends Plugin {\n    static id = \"scrollButtonOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(SCROLL_BUTTON, ScrollButtonOption)],\n        builder_actions: {\n            AddScrollButtonAction,\n            ScrollButtonSectionHeightClassAction,\n        },\n    };\n}\n\nclass ScrollButtonManager {\n    constructor() {\n        this.buttonCache = new Map();\n    }\n\n    createButton() {\n        const anchor = document.createElement(\"a\");\n        anchor.classList.add(\n            \"o_scroll_button\",\n            \"mb-3\",\n            \"rounded-circle\",\n            \"align-items-center\",\n            \"justify-content-center\",\n            \"mx-auto\",\n            \"bg-primary\",\n            \"o_not_editable\"\n        );\n        anchor.href = \"#\";\n        anchor.contentEditable = \"false\";\n        anchor.title = _t(\"Scroll down to next section\");\n\n        const arrow = document.createElement(\"i\");\n        arrow.classList.add(\"fa\", \"fa-angle-down\", \"fa-3x\");\n        anchor.appendChild(arrow);\n\n        return anchor;\n    }\n\n    ensureButton(editingElement) {\n        let button = this.buttonCache.get(editingElement);\n        if (!button) {\n            button = this.createButton();\n            this.buttonCache.set(editingElement, button);\n        }\n        return button;\n    }\n\n    attachButton(editingElement) {\n        const button = this.ensureButton(editingElement);\n        editingElement.appendChild(button);\n    }\n\n    removeButton(editingElement) {\n        const button = editingElement.querySelector(\":scope > .o_scroll_button\");\n        if (button) {\n            button.remove();\n            this.buttonCache.set(editingElement, button); // Cache for reuse\n        }\n    }\n\n    isButtonPresent(editingElement) {\n        return !!editingElement.querySelector(\":scope > .o_scroll_button\");\n    }\n}\n\nconst scrollButtonManager = new ScrollButtonManager();\n\nexport class AddScrollButtonAction extends BuilderAction {\n    static id = \"addScrollButton\";\n    setup() {\n        this.manager = scrollButtonManager;\n    }\n\n    isApplied({ editingElement }) {\n        return this.manager.isButtonPresent(editingElement);\n    }\n\n    apply({ editingElement }) {\n        this.manager.attachButton(editingElement);\n    }\n\n    clean({ editingElement }) {\n        this.manager.removeButton(editingElement);\n    }\n}\n\nexport class ScrollButtonSectionHeightClassAction extends ClassAction {\n    static id = \"scrollButtonSectionHeightClass\";\n    setup() {\n        this.manager = scrollButtonManager;\n    }\n\n    apply({ editingElement, params: { mainParam } }) {\n        super.apply(...arguments);\n        if (mainParam) {\n            editingElement.classList.replace(\"d-lg-block\", \"d-lg-flex\");\n        } else if (editingElement.classList.contains(\"d-lg-flex\")) {\n            editingElement.classList.remove(\"d-lg-flex\");\n            const style = window.getComputedStyle(editingElement);\n            const display = style.getPropertyValue(\"display\");\n            editingElement.classList.add(display === \"flex\" ? \"d-lg-flex\" : \"d-lg-block\");\n        }\n    }\n\n    clean(args) {\n        super.clean(args);\n        if (args.params.mainParam === \"o_full_screen_height\") {\n            this.manager.removeButton(args.editingElement);\n        }\n    }\n}\n\nregistry.category(\"website-plugins\").add(ScrollButtonOptionPlugin.id, ScrollButtonOptionPlugin);\n", "import { BaseOptionComponent, useGetItemValue } from \"@html_builder/core/utils\";\n\nexport class SearchbarOption extends BaseOptionComponent {\n    static template = \"website.SearchbarOption\";\n    static selector = \".s_searchbar_input\";\n    static applyTo = \".search-query\";\n\n    setup() {\n        super.setup();\n        this.getItemValue = useGetItemValue();\n\n        this.orderByItems = this.getResource(\"searchbar_option_order_by_items\");\n        this.displayItems = this.getResource(\"searchbar_option_display_items\");\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { SearchbarOption } from \"./searchbar_option\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\n\n/** @typedef {import(\"plugins\").TranslatedString} TranslatedString */\n\n/**\n * @typedef {{\n *      label: TranslatedString;\n *      orderBy: string;\n *      dependency?: string;\n *      id?: string;\n * }[]} searchbar_option_order_by_items\n *\n * Register orderBy options for the website searchbar.\n * `orderBy` takes a string like `record_field_name` + `asc` or `desc`.\n * `dependency` takes an id of another builder option. You can omit it if the\n * orderBy option should always be visible.\n * You can reference `id` if you need the new option to have an id (if another\n * option depends on it being active).\n *\n * Example:\n *\n *      resources: {\n *          searchbar_option_order_by_items: {\n *              label: _t(\"Date (old to new)\"),\n *              orderBy: \"published_date asc\",\n *              dependency: \"search_blogs_opt\",\n *          },\n *      };\n */\n/**\n * @typedef {{\n *      label: TranslatedString;\n *      dataAttribute: string;\n *      dependency: string;\n * }[]} searchbar_option_display_items\n *\n * Register display options for the website searchbar.\n * `dataAttribute` is the attribute which will be used to display the data.\n * `dependency` takes an id of another builder option.\n *\n * Example:\n *\n *      resources: {\n *          searchbar_option_display_items: {\n *              label: _t(\"Description\"),\n *              dataAttribute: \"displayDescription\",\n *              dependency: \"search_all_opt\",\n *          },\n *      };\n */\n\nclass SearchbarOptionPlugin extends Plugin {\n    static id = \"searchbarOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [SearchbarOption],\n        builder_actions: {\n            SetSearchTypeAction,\n            SetOrderByAction,\n            SetSearchbarStyleAction,\n            // This resets the data attribute to an empty string on clean.\n            // TODO: modify the Python `_search_get_detail()` (grep\n            // `with_description = options['displayDescription']`) so we can use\n            // the default `dataAttributeAction`. The python should not need a\n            // value if it doesn't exist.\n            SetNonEmptyDataAttributeAction,\n        },\n        so_content_addition_selector: [\".s_searchbar_input\"],\n        searchbar_option_order_by_items: {\n            label: _t(\"Name (A-Z)\"),\n            orderBy: \"name asc\",\n            id: \"order_name_asc_opt\",\n        },\n        searchbar_option_display_items: [\n            {\n                label: _t(\"Description\"),\n                dataAttribute: \"displayDescription\",\n                dependency: \"search_all_opt\",\n            },\n            {\n                label: _t(\"Content\"),\n                dataAttribute: \"displayDescription\",\n                dependency: \"search_pages_opt\",\n            },\n            {\n                label: _t(\"Extra Link\"),\n                dataAttribute: \"displayExtraLink\",\n                dependency: \"search_all_opt\",\n            },\n            {\n                label: _t(\"Detail\"),\n                dataAttribute: \"displayDetail\",\n                dependency: \"search_all_opt\",\n            },\n            {\n                label: _t(\"Image\"),\n                dataAttribute: \"displayImage\",\n                dependency: \"search_all_opt\",\n            },\n        ],\n        // input group should not be contenteditable, while all other children\n        // beside the input are contenteditable\n        content_not_editable_selectors: [\".input-group:has( > input)\"],\n        content_editable_selectors: [\".input-group:has( > input) > *:not(input)\"],\n    };\n}\n\nexport class BaseSearchBarAction extends BuilderAction {\n    id = \"baseSearchBar\";\n    defaultSearchType = \"name asc\";\n\n    getFormEl(editingElement) {\n        return editingElement.closest(\"form\");\n    }\n    getSearchButtonEl(editingElement) {\n        // /!\\ this could return undefined if the button was deleted.\n        return editingElement.closest(\".s_searchbar_input\").querySelector(\".oe_search_button\");\n    }\n    getSearchOrderByInputEl(editingElement) {\n        return this.getFormEl(editingElement).querySelector(\".o_search_order_by\");\n    }\n}\nexport class SetSearchTypeAction extends BaseSearchBarAction {\n    static id = \"setSearchType\";\n    apply({ editingElement, value: formAction, dependencyManager }) {\n        this.getFormEl(editingElement).action = formAction;\n\n        const isDependencyActive = (dep) => !dep || dependencyManager.get(dep).isActive();\n\n        // If the selected orderBy option is not available with the\n        // new search type, reset to default.\n        const searchOrderByInputEl = this.getSearchOrderByInputEl(editingElement);\n        if (\n            !this.getResource(\"searchbar_option_order_by_items\").some(\n                (item) =>\n                    isDependencyActive(item.dependency) &&\n                    item.orderBy === searchOrderByInputEl.value\n            )\n        ) {\n            editingElement.dataset.orderBy = this.defaultSearchType;\n            searchOrderByInputEl.value = this.defaultSearchType;\n        }\n\n        // Reset display options. Has to be done in 2 steps, because\n        // the same option may be on 2 dependencies, and we don't\n        // want the 1st to add it and the 2nd to delete it.\n        const displayDataAttributes = new Set();\n        for (const item of this.getResource(\"searchbar_option_display_items\")) {\n            if (isDependencyActive(item.dependency)) {\n                displayDataAttributes.add(item.dataAttribute);\n            } else {\n                delete editingElement.dataset[item.dataAttribute];\n            }\n        }\n        for (const dataAttribute of displayDataAttributes) {\n            editingElement.dataset[dataAttribute] = \"true\";\n        }\n    }\n}\nexport class SetOrderByAction extends BaseSearchBarAction {\n    static id = \"setOrderBy\";\n    apply({ editingElement, value: orderBy }) {\n        this.getSearchOrderByInputEl(editingElement).value = orderBy;\n    }\n}\n\nexport class SetSearchbarStyleAction extends BaseSearchBarAction {\n    static id = \"setSearchbarStyle\";\n    isApplied({ editingElement, params: { mainParam: style } }) {\n        const searchInputIsLight = editingElement.matches(\".border-0.bg-light\");\n        const searchButtonIsLight = this.getSearchButtonEl(editingElement)?.matches(\".btn-light\");\n\n        if (style === \"light\") {\n            return searchInputIsLight && searchButtonIsLight;\n        }\n        if (style === \"default\") {\n            return !searchInputIsLight && !searchButtonIsLight;\n        }\n    }\n    apply({ editingElement, params: { mainParam: style } }) {\n        const isLight = style === \"light\";\n        const searchButtonEl = this.getSearchButtonEl(editingElement);\n        editingElement.classList.toggle(\"border-0\", isLight);\n        editingElement.classList.toggle(\"bg-light\", isLight);\n        searchButtonEl?.classList.toggle(\"btn-light\", isLight);\n        searchButtonEl?.classList.toggle(\"btn-primary\", !isLight);\n    }\n}\n// This resets the data attribute to an empty string on clean.\n// TODO: modify the Python `_search_get_detail()` (grep\n// `with_description = options['displayDescription']`) so we can use\n// the default `dataAttributeAction`. The python should not need a\n// value if it doesn't exist.\nexport class SetNonEmptyDataAttributeAction extends BuilderAction {\n    static id = \"setNonEmptyDataAttribute\";\n    getValue({ editingElement, params: { mainParam: attributeName } = {} }) {\n        return editingElement.dataset[attributeName];\n    }\n    isApplied({ editingElement, params: { mainParam: attributeName } = {}, value = \"\" }) {\n        return editingElement.dataset[attributeName] === value;\n    }\n    apply({ editingElement, params: { mainParam: attributeName } = {}, value }) {\n        if (value) {\n            editingElement.dataset[attributeName] = value;\n        } else {\n            delete editingElement.dataset[attributeName];\n        }\n    }\n    clean({ editingElement, params: { mainParam: attributeName } = {} }) {\n        editingElement.dataset[attributeName] = \"\";\n    }\n}\n\nregistry.category(\"website-plugins\").add(SearchbarOptionPlugin.id, SearchbarOptionPlugin);\n", "import { useDomState, BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { onWillStart, useRef, useState } from \"@odoo/owl\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\n\nexport class SocialMediaLinks extends BaseOptionComponent {\n    static template = \"website.SocialMediaLinks\";\n    static dependencies = [\"socialMediaOptionPlugin\", \"history\"];\n    static selector = \".s_social_media\";\n\n    setup() {\n        super.setup();\n\n        const { getRecordedSocialMediaNames, reorderSocialMediaLink } =\n            this.dependencies.socialMediaOptionPlugin;\n\n        onWillStart(async () => {\n            this.recordedSocialMediaNames = await getRecordedSocialMediaNames();\n        });\n        this.rootRef = useRef(\"root\");\n        this.domState = useDomState((editingElement) => ({\n            presentLinks: [...editingElement.querySelectorAll(\":scope > a[href]\")].map(\n                (element) => ({\n                    element,\n                    media: element.attributes.href.value.split(\"/website/social/\")[1],\n                })\n            ),\n        }));\n\n        this.nextId = 1001;\n        this.ids = [];\n        this.elIdsMap = new Map();\n        this.idsElMap = new Map();\n        this.idsMediaMap = new Map();\n        this.mediaIdsMap = new Map();\n\n        // hack to trigger the rebuild\n        this.reorderTriggered = useState({ trigger: 0 });\n\n        useSortable({\n            ref: this.rootRef,\n            elements: \"tr\",\n            handle: \".o_drag_handle\",\n            cursor: \"grabbing\",\n            placeholderClasses: [\"d-table-row\"],\n\n            onDrop: ({ next, element }) => {\n                const elId = parseInt(element.dataset.id);\n                const nextId = next?.dataset.id;\n\n                const oldIdx = this.ids.findIndex((id) => id === elId);\n                this.ids.splice(oldIdx, 1);\n                const oldNext = this.ids\n                    .slice(oldIdx)\n                    .find((i) => this.idsElMap.get(i)?.isConnected);\n                let idx = this.ids.findIndex((id) => id == nextId);\n                if (0 <= idx) {\n                    this.ids.splice(idx, 0, elId);\n                } else {\n                    idx = this.ids.length;\n                    this.ids.push(elId);\n                }\n                const newNext = this.ids\n                    .slice(idx + 1)\n                    .find((i) => this.idsElMap.get(i)?.isConnected);\n\n                if (this.idsElMap.get(elId)?.isConnected && oldNext !== newNext) {\n                    reorderSocialMediaLink({\n                        editingElement: this.env.getEditingElement(),\n                        element: this.idsElMap.get(elId),\n                        elementAfter: this.idsElMap.get(newNext),\n                    });\n                    this.dependencies.history.addStep();\n                }\n\n                // hack to trigger the rebuild\n                this.reorderTriggered.trigger++;\n            },\n        });\n    }\n\n    /**\n     * Each item has at least one of `domPosition` or `media`\n     * @typedef { Object } SocialMediaLinkItem\n     * @property { String } fabricatedKey a key that combines the `id` and the `domPosition` (this is a hack to trigger rebuild when domPosition changes, because `applyTo does not correctly support props updates)\n     * @property { int } id An arbitrary number to identify an item\n     * @property { int } [domPosition] The position of the link in the children list (if the item has a link in the dom), starting from 1 (to use `:nth-` selector)\n     * @property { string } [media] The name of the recorded social media (if the item is editing a link from the orm)\n     */\n\n    /**\n     * Builds the list of items by reconciling what is present in the dom with what was previously computed\n     * @returns { SocialMediaLinkItem[] }\n     */\n    computeItems() {\n        const missingRecordedSocialMediaNames = new Set(this.recordedSocialMediaNames);\n        const idsLookUp = new Map(this.ids.map((id, i) => [id, i]));\n        const idsInDom = new Set();\n        const itemsFromDom = this.domState.presentLinks.map(({ element, media }, domPosition) => {\n            let id = this.elIdsMap.get(element);\n            if (!id) {\n                const idBasedOnMedia = this.mediaIdsMap.get(media);\n                if (!idsInDom.has(idBasedOnMedia)) {\n                    id = idBasedOnMedia;\n                }\n            }\n            if (!id) {\n                id = this.nextId++;\n            }\n            idsInDom.add(id);\n            if (media) {\n                missingRecordedSocialMediaNames.delete(media);\n            }\n            return { element, media, id, domPosition: domPosition + 1 };\n        });\n        const items = [];\n        const addRecordedSocialMediaAtStartOfSlice = (slice) => {\n            for (const id of slice) {\n                if (idsInDom.has(id)) {\n                    break;\n                }\n                const media = this.idsMediaMap.get(id);\n                if (media) {\n                    items.push({ id, media });\n                    missingRecordedSocialMediaNames.delete(media);\n                }\n            }\n        };\n        addRecordedSocialMediaAtStartOfSlice(this.ids);\n        for (const item of itemsFromDom) {\n            items.push(item);\n            const start = idsLookUp.get(item.id);\n            if (start !== undefined) {\n                addRecordedSocialMediaAtStartOfSlice(this.ids.slice(start + 1));\n            }\n        }\n        for (const media of missingRecordedSocialMediaNames) {\n            items.push({ id: this.nextId++, media });\n        }\n\n        this.ids = [];\n        this.elIdsMap = new Map();\n        this.idsMediaMap = new Map();\n\n        for (const item of items) {\n            this.ids.push(item.id);\n            if (item.element) {\n                this.elIdsMap.set(item.element, item.id);\n                this.idsElMap.set(item.id, item.element);\n            }\n            if (item.media) {\n                this.idsMediaMap.set(item.id, item.media);\n                this.mediaIdsMap.set(item.media, item.id);\n            }\n        }\n        let elementAfter = null;\n        for (let i = items.length - 1; i >= 0; i--) {\n            items[i].nextLink = elementAfter;\n            // This fabricated key is a hack. It is used as `t-key` in the component instead of the id in order to force re-creation of the components if the domPosition changes (this re-creation is a workaround for the applyTo that are not correctly updated)\n            items[i].fabricatedKey = `${items[i].id}+${items[i].domPosition}`;\n            if (items[i].element) {\n                elementAfter = items[i].element;\n                delete items[i].element;\n            }\n        }\n\n        return items;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { ICON_SELECTOR } from \"@html_editor/utils/dom_info\";\nimport { fonts } from \"@html_editor/utils/fonts\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { renderToFragment } from \"@web/core/utils/render\";\nimport { SocialMediaLinks } from \"./social_media_links\";\nimport { selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { SNIPPET_SPECIFIC, TITLE_LAYOUT_SIZE } from \"@html_builder/utils/option_sequence\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\n/**\n * @typedef { Object } SocialMediaOptionShared\n * @property { SocialMediaOptionPlugin['newLinkElement'] } newLinkElement\n * @property { SocialMediaOptionPlugin['getRecordedSocialMedia'] } getRecordedSocialMedia\n * @property { SocialMediaOptionPlugin['setRecordedSocialMedia'] } setRecordedSocialMedia\n * @property { SocialMediaOptionPlugin['setRecordedSocialMediaAreEdited'] } setRecordedSocialMediaAreEdited\n * @property { SocialMediaOptionPlugin['getAssociatedSocialMedia'] } getAssociatedSocialMedia\n * @property { SocialMediaOptionPlugin['removeSocialMediaClasses'] } removeSocialMediaClasses\n * @property { SocialMediaOptionPlugin['removeIconClasses'] } removeIconClasses\n * @property { SocialMediaOptionPlugin['getRecordedSocialMediaNames'] } getRecordedSocialMediaNames\n * @property { SocialMediaOptionPlugin['reorderSocialMediaLink'] } reorderSocialMediaLink\n */\n\n/**\n * @typedef { Object } SocialMediaInfo\n * @property { boolean } [recorded] whether the social media is one from the orm\n * @property { import(\"plugins\").TranslatedString } label\n * @property { string } iconClass the icon class to use for the social media\n * @property { RegExp } [extraHostnameRegex] a regex for host names that belongs to this social media, but are not catch by the default mechanism\n */\n\n/** @type { Map<string, SocialMediaInfo> } */\nconst socialMediaInfo = new Map(\n    Object.entries({\n        facebook: {\n            recorded: true,\n            label: _t(\"Facebook\"),\n            iconClass: \"fa-facebook\",\n            extraHostnameRegex: /(^|\\.)fb\\.(com|me)$/,\n        },\n        twitter: {\n            recorded: true,\n            label: _t(\"X\"),\n            iconClass: \"fa-twitter\",\n            extraHostnameRegex: /(^|\\.)x\\.com$/,\n        },\n        linkedin: {\n            recorded: true,\n            label: _t(\"LinkedIn\"),\n            iconClass: \"fa-linkedin\",\n        },\n        youtube: {\n            recorded: true,\n            label: _t(\"YouTube\"),\n            iconClass: \"fa-youtube-play\",\n            extraHostnameRegex: /(^|\\.)youtu\\.be$/,\n        },\n        instagram: {\n            recorded: true,\n            label: _t(\"Instagram\"),\n            iconClass: \"fa-instagram\",\n            extraHostnameRegex: /(^|\\.)instagr\\.(am|com)$/,\n        },\n        github: {\n            recorded: true,\n            label: _t(\"GitHub\"),\n            iconClass: \"fa-github\",\n        },\n        tiktok: {\n            recorded: true,\n            label: _t(\"TikTok\"),\n            iconClass: \"fa-tiktok\",\n        },\n        discord: {\n            recorded: true,\n            label: _t(\"Discord\"),\n            iconClass: \"fa-discord\",\n        },\n        \"google-play\": {\n            label: _t(\"Google Play\"),\n            iconClass: \"fa-google-play\",\n            // Without this, the default finds 'google' instead\n            extraHostnameRegex: /(^|\\.)play\\.google\\.com$/,\n        },\n        google: {\n            label: _t(\"Google\"),\n            iconClass: \"fa-google\",\n        },\n        whatsapp: {\n            label: _t(\"Whatsapp\"),\n            iconClass: \"fa-whatsapp\",\n            extraHostnameRegex: /(^|\\.)wa\\.me$/,\n        },\n        pinterest: {\n            label: _t(\"Pinterest\"),\n            iconClass: \"fa-pinterest-p\",\n        },\n        kickstarter: {\n            label: _t(\"Kickstarter\"),\n            iconClass: \"fa-kickstarter\",\n        },\n        strava: {\n            label: _t(\"Strava\"),\n            iconClass: \"fa-strava\",\n        },\n        bluesky: {\n            label: _t(\"Bluesky\"),\n            iconClass: \"fa-bluesky\",\n            extraHostnameRegex: /(^|\\.)bsky\\.(app|social)$/,\n        },\n        threads: {\n            label: _t(\"Threads\"),\n            iconClass: \"fa-threads\",\n        },\n    })\n);\n\nconst defaultAriaLabel = _t(\"Other social network\");\n\nexport class SocialMediaOption extends BaseOptionComponent {\n    static template = \"website.SocialMediaOption\";\n    static selector = \".s_share, .s_social_media\";\n}\n\nclass SocialMediaOptionPlugin extends Plugin {\n    static id = \"socialMediaOptionPlugin\";\n    static dependencies = [\"history\"];\n    static shared = [\n        \"newLinkElement\",\n        \"getRecordedSocialMedia\",\n        \"setRecordedSocialMedia\",\n        \"setRecordedSocialMediaAreEdited\",\n        \"getAssociatedSocialMedia\",\n        \"removeSocialMediaClasses\",\n        \"removeIconClasses\",\n        \"getRecordedSocialMediaNames\",\n        \"reorderSocialMediaLink\",\n    ];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(TITLE_LAYOUT_SIZE, SocialMediaOption),\n            withSequence(SNIPPET_SPECIFIC, SocialMediaLinks),\n        ],\n        so_content_addition_selector: [\".s_share\", \".s_social_media\"],\n        builder_actions: {\n            DeleteSocialMediaLinkAction,\n            ToggleRecordedSocialMediaLinkAction,\n            EditRecordedSocialMediaLinkAction,\n            EditSocialMediaLinkAction,\n            AddSocialMediaLinkAction,\n        },\n        normalize_handlers: this.normalize.bind(this),\n        save_handlers: this.saveRecordedSocialMedia.bind(this),\n        content_not_editable_selectors: [\".s_share\"],\n        content_editable_selectors: [\n            \".s_share a > i\",\n            \".s_share .s_share_title\",\n            \".s_social_media a > i\",\n            \".s_social_media .s_social_media_title\",\n        ],\n    };\n\n    /** The social media's name for which there is an entry in the orm */\n    async getRecordedSocialMediaNames() {\n        await this.fetchRecordedSocialMedia();\n        return this.recordedSocialMedia.keys();\n    }\n\n    // TODO: a method to give access to the `recordedSocialMedia` for facebook page and instagram page\n\n    setup() {\n        this.recordedSocialMedia = new Map();\n    }\n\n    getRecordedSocialMedia(key) {\n        return this.recordedSocialMedia.get(key);\n    }\n    setRecordedSocialMedia(key, value) {\n        this.recordedSocialMedia.set(key, value);\n    }\n    setRecordedSocialMediaAreEdited(value) {\n        this.recordedSocialMediaAreEdited = value;\n    }\n    async fetchRecordedSocialMedia() {\n        if (this.hasStartedLoadingRecordedSocialMedia) {\n            return;\n        }\n        this.hasStartedLoadingRecordedSocialMedia = true;\n\n        const res = await this.services.orm.read(\n            \"website\",\n            [this.services.website.currentWebsite.id],\n            [\n                ...socialMediaInfo\n                    .entries()\n                    .filter(([name, info]) => info.recorded)\n                    .map(([name, info]) => `social_${name}`),\n            ]\n        );\n        for (const name of socialMediaInfo.keys()) {\n            const key = `social_${name}`;\n            if (key in res[0]) {\n                this.recordedSocialMedia.set(name, res[0][key] || \"\");\n            }\n        }\n        this.config.onChange({ isPreviewing: false });\n    }\n\n    async saveRecordedSocialMedia() {\n        if (!this.recordedSocialMediaAreEdited) {\n            return;\n        }\n        await this.services.orm.write(\n            \"website\",\n            [this.services.website.currentWebsite.id],\n            Object.fromEntries(\n                this.recordedSocialMedia.entries().map(([name, value]) => [`social_${name}`, value])\n            )\n        );\n\n        this.recordedSocialMediaAreEdited = false;\n    }\n\n    normalize(root) {\n        // Add https:// if needed, to the links from db, and the links from dom\n        if (this.recordedSocialMediaAreEdited) {\n            for (const [name, value] of this.recordedSocialMedia.entries()) {\n                const newValue = this.addHttpsIfNeeded(value);\n                if (value !== newValue) {\n                    this.recordedSocialMedia.set(name, newValue);\n                }\n            }\n        }\n        for (const element of selectElements(root, \".s_social_media > a[href]\")) {\n            const value = element.attributes.href.value;\n            const newHref = this.addHttpsIfNeeded(value);\n            if (value !== newHref) {\n                element.href = newHref;\n            }\n        }\n\n        // ensure one '\\n' between each element + before and after\n        for (const element of selectElements(root, \".s_social_media > *\")) {\n            if (element.nextSibling?.nodeType === Node.TEXT_NODE) {\n                while (element.nextSibling.nextSibling?.nodeType === Node.TEXT_NODE) {\n                    element.parentNode.removeChild(element.nextSibling);\n                }\n                if (element.nextSibling.textContent !== \"\\n\") {\n                    element.nextSibling.textContent = \"\\n\";\n                }\n            } else {\n                element.after(\"\\n\");\n            }\n            if (element.previousSibling?.nodeType !== Node.TEXT_NODE) {\n                element.before(\"\\n\");\n            }\n        }\n    }\n\n    /**\n     * @param { HTMLElement } editingElement The element edited\n     * @param { HTMLElement } element The element that is moved (a child of `editingElement`)\n     * @param { HTMLElement } [elementAfter] The element that should be after the moved element (not present if moved to the end)\n     */\n    reorderSocialMediaLink({ editingElement, element, elementAfter }) {\n        element.remove();\n        if (elementAfter) {\n            elementAfter.before(element);\n        } else {\n            editingElement.append(element);\n        }\n    }\n\n    /**\n     * @param { HTMLElement } [other] a link element to clone to use as base (use the template if none)\n     * @param { String } [socialMediaName] the name of the social media to use if any\n     * @returns { HTMLElement } a new link element\n     */\n    newLinkElement(other, socialMediaName) {\n        const el =\n            other?.cloneNode(true) ||\n            renderToFragment(\"website.example_social_media_link\").children[0];\n        this.removeSocialMediaClasses(el);\n        this.removeIconClasses(el);\n        el.querySelector(ICON_SELECTOR)?.classList.add(\n            socialMediaInfo.get(socialMediaName)?.iconClass || \"fa-pencil\"\n        );\n        if (socialMediaName) {\n            el.href = `/website/social/${encodeURIComponent(socialMediaName)}`;\n            el.classList.add(`s_social_media_${socialMediaName}`);\n            el.setAttribute(\n                \"aria-label\",\n                socialMediaInfo.get(socialMediaName)?.label || defaultAriaLabel\n            );\n        } else {\n            el.href = \"https://www.example.com\";\n            el.setAttribute(\"aria-label\", \"example\");\n        }\n        return el;\n    }\n\n    /**\n     * Strip an element from the classes associated to social media\n     * @param { HTMLElement } el\n     */\n    removeSocialMediaClasses(el) {\n        for (const c of el.classList) {\n            if (c.startsWith(\"s_social_media_\")) {\n                el.classList.remove(c);\n            }\n        }\n    }\n    /**\n     * Strip an element from the classes associated to an icon (keeps the size)\n     * @param { HTMLElement } el\n     */\n    removeIconClasses(el) {\n        const iconEl = el.querySelector(ICON_SELECTOR);\n        if (iconEl) {\n            // Remove every fa classes except fa-x sizes.\n            for (const c of iconEl.classList) {\n                if (/^fa-[^0-9]/.test(c)) {\n                    iconEl.classList.remove(c);\n                }\n            }\n        }\n    }\n\n    /**\n     * @typedef { Object } AssociatedSocialMediaReturn\n     * @property { String } [name] the name of the social media\n     * @property { SocialMediaInfo } [media] the info about the social media (an entry of `socialMediaInfo`) @see socialMediaInfo\n     */\n    /**\n     * @param { String } link\n     * @returns { AssociatedSocialMediaReturn }\n     */\n    getAssociatedSocialMedia(link) {\n        try {\n            const url = new URL(this.addHttpsIfNeeded(link));\n            if (url.protocol && !url.protocol.startsWith(\"http\")) {\n                return {}; // no mailto, etc\n            }\n            const hostname = url.hostname;\n            for (const [name, media] of socialMediaInfo.entries()) {\n                if (media.extraHostnameRegex?.test(hostname)) {\n                    return { name, media };\n                }\n            }\n            // Retrieve the domain of the given url.\n            const name = hostname\n                .replace(/\\.co\\.uk$/, \".co\")\n                .split(\".\")\n                .slice(-2)[0];\n            return { name, media: socialMediaInfo.get(name) };\n        } catch {\n            return {};\n        }\n    }\n\n    /**\n     * @param { String } link\n     * @returns { String } the same link, prefixed with 'https://' if none is set\n     */\n    addHttpsIfNeeded(link) {\n        // We permit every protocol (http:, https:, ftp:, mailto:,...).\n        // If none is explicitly specified, we assume it is a https.\n        if (link && !/^(([a-zA-Z]+):|\\/)/.test(link)) {\n            return `https://${link}`;\n        } else {\n            return link;\n        }\n    }\n}\n\nexport class DeleteSocialMediaLinkAction extends BuilderAction {\n    static id = \"deleteSocialMediaLink\";\n    apply({ editingElement }) {\n        editingElement.remove();\n    }\n}\nexport class ToggleRecordedSocialMediaLinkAction extends BuilderAction {\n    static id = \"toggleRecordedSocialMediaLink\";\n    static dependencies = [\"socialMediaOptionPlugin\"];\n    isApplied({ editingElement, params: { domPosition } }) {\n        return !!domPosition;\n    }\n    apply({ editingElement, params: { media, elementAfter } }) {\n        const el = this.dependencies.socialMediaOptionPlugin.newLinkElement(\n            editingElement.querySelector(\":scope > a\"),\n            media\n        );\n        if (elementAfter) {\n            elementAfter.before(el);\n        } else {\n            editingElement.append(el);\n        }\n    }\n    clean({ editingElement, params: { domPosition } }) {\n        editingElement.querySelector(`a:nth-of-type(${domPosition})`).remove();\n    }\n}\nexport class EditRecordedSocialMediaLinkAction extends BuilderAction {\n    static id = \"editRecordedSocialMediaLink\";\n    static dependencies = [\"socialMediaOptionPlugin\", \"history\"];\n    getValue({ params: { mainParam } }) {\n        return this.dependencies.socialMediaOptionPlugin.getRecordedSocialMedia(mainParam);\n    }\n    apply({ params: { mainParam }, value }) {\n        this.dependencies.socialMediaOptionPlugin.setRecordedSocialMediaAreEdited(true);\n        const oldValue =\n            this.dependencies.socialMediaOptionPlugin.getRecordedSocialMedia(mainParam);\n        this.dependencies.history.applyCustomMutation({\n            apply: () =>\n                this.dependencies.socialMediaOptionPlugin.setRecordedSocialMedia(mainParam, value),\n            revert: () =>\n                this.dependencies.socialMediaOptionPlugin.setRecordedSocialMedia(\n                    mainParam,\n                    oldValue\n                ),\n        });\n    }\n}\nexport class EditSocialMediaLinkAction extends BuilderAction {\n    static id = \"editSocialMediaLink\";\n    static dependencies = [\"socialMediaOptionPlugin\"];\n    apply({ editingElement, params: { mainParam }, value }) {\n        if (!value) {\n            editingElement.remove();\n        }\n        const info = this.dependencies.socialMediaOptionPlugin.getAssociatedSocialMedia(value);\n        const ariaLabel = info.media?.label || info.name || defaultAriaLabel;\n        editingElement.setAttribute(\"aria-label\", ariaLabel);\n\n        this.dependencies.socialMediaOptionPlugin.removeSocialMediaClasses(editingElement);\n        let iconClass;\n        if (info.media) {\n            editingElement.classList.add(`s_social_media_${info.name}`);\n            iconClass = info.media.iconClass;\n        } else if (info.name) {\n            fonts.computeFonts();\n            iconClass = fonts.fontIcons[0].alias\n                .filter((el) => el.replace(/^fa-/, \"\").includes(info.name))\n                .reduce((a, b) => (a.length && a.length <= b.length ? a : b), \"\");\n        }\n\n        if (iconClass) {\n            this.dependencies.socialMediaOptionPlugin.removeIconClasses(editingElement);\n            editingElement.querySelector(ICON_SELECTOR)?.classList.add(iconClass);\n        }\n    }\n}\nexport class AddSocialMediaLinkAction extends BuilderAction {\n    static id = \"addSocialMediaLink\";\n    static dependencies = [\"socialMediaOptionPlugin\"];\n    apply({ editingElement }) {\n        editingElement.append(\n            this.dependencies.socialMediaOptionPlugin.newLinkElement(\n                editingElement.querySelector(\":scope > a\")\n            )\n        );\n    }\n}\n\nregistry.category(\"website-plugins\").add(SocialMediaOptionPlugin.id, SocialMediaOptionPlugin);\n", "import { applyFunDependOnSelectorAndExclude } from \"@html_builder/plugins/utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\n/**\n * Returns the TOC id and the heading id from a header element.\n *\n * @param {HTMLElement} headingEl - A header element of the TOC.\n * @returns {Object}\n */\nfunction getTocAndHeadingId(headingEl) {\n    const match = /^table_of_content_heading_(\\d+)_(\\d+)$/.exec(\n        headingEl && headingEl.getAttribute(\"id\")\n    );\n    if (match) {\n        return { tocId: parseInt(match[1]), headingId: parseInt(match[2]) };\n    }\n    return { tocId: 0, headingId: 0 };\n}\n\nexport class TableOfContentOption extends BaseOptionComponent {\n    static template = \"website.TableOfContentOption\";\n    static selector = \".s_table_of_content\";\n}\n\nexport class TableOfContentNavbarOption extends BaseOptionComponent {\n    static template = \"website.TableOfContentNavbarOption\";\n    static selector = \".s_table_of_content_navbar_wrap\";\n}\n\nclass TableOfContentOptionPlugin extends Plugin {\n    static id = \"tableOfContentOption\";\n    static dependencies = [\"remove\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [TableOfContentOption, TableOfContentNavbarOption],\n        builder_actions: {\n            NavbarPositionAction,\n        },\n        normalize_handlers: this.normalize.bind(this),\n        // Prevent dropping a table of content inside another table of content.\n        dropzone_selector: {\n            selector: \".s_table_of_content\",\n            excludeAncestor: \".s_table_of_content\",\n        },\n        // Only allow moving main parts of the table of content by using arrows.\n        is_draggable_handlers: (el) => {\n            if (\n                el.matches(\n                    \".s_table_of_content .s_table_of_content_navbar_wrap, .s_table_of_content .s_table_of_content_main\"\n                )\n            ) {\n                return false;\n            }\n            return true;\n        },\n        is_unremovable_selector: \".s_table_of_content_navbar_wrap, .s_table_of_content_main\",\n        content_not_editable_selectors: \".s_table_of_content_navbar\",\n    };\n\n    normalize(root) {\n        applyFunDependOnSelectorAndExclude(this.updateTableOfContentNavbar.bind(this), root, {\n            selector: \".s_table_of_content_main\",\n        });\n    }\n\n    updateTableOfContentNavbar(tableOfContentMain) {\n        const tableOfContent = tableOfContentMain.closest(\".s_table_of_content\");\n        const tableOfContentNavbar = tableOfContent.querySelector(\".s_table_of_content_navbar\");\n        const currentNavbarItems = [...tableOfContentNavbar.children].map((el) => ({\n            title: el.textContent,\n            href: el.getAttribute(\"href\"),\n        }));\n\n        if (tableOfContentMain.children.length === 0) {\n            // Remove the table of content if empty content.\n            this.dependencies.remove.removeElement(tableOfContent);\n            return;\n        }\n\n        const targetedElements = \"h1, h2\";\n        const currentHeadingItems = [...tableOfContentMain.querySelectorAll(targetedElements)]\n            .filter((el) => !el.closest(\".o_snippet_desktop_invisible\"))\n            .map((el) => ({ title: el.textContent, id: `#${el.id}`, el }));\n\n        const headingHasChanged =\n            currentNavbarItems.length !== currentHeadingItems.length ||\n            currentNavbarItems.some(\n                (item, i) =>\n                    item.title !== currentHeadingItems[i].title ||\n                    item.href !== currentHeadingItems[i].id\n            );\n\n        const areVisibilityIdsEqual = currentHeadingItems.every(({ el }) => {\n            const visibilityId = el.closest(\"section\").getAttribute(\"data-visibility-id\");\n            const matchingLinkEl = tableOfContentNavbar.querySelector(\n                `a[href=\"#${el.getAttribute(\"id\")}\"]`\n            );\n            const matchingLinkVisibilityId = matchingLinkEl\n                ? matchingLinkEl.getAttribute(\"data-visibility-id\")\n                : null;\n            // Check if visibilityId matches matchingLinkVisibilityId or both\n            // are null/undefined\n            return visibilityId === matchingLinkVisibilityId;\n        });\n\n        const firstHeadingEl = currentHeadingItems[0]?.el;\n        let tocId = firstHeadingEl ? getTocAndHeadingId(firstHeadingEl).tocId : 0;\n        const tocEls = this.editable.querySelectorAll(\"[data-snippet='s_table_of_content']\");\n        const otherTocEls = [...tocEls].filter((tocEl) => tocEl !== tableOfContent);\n        const otherTocIds = otherTocEls.map((tocEl) => {\n            const firstHeadingEl = tocEl.querySelector(targetedElements);\n            return getTocAndHeadingId(firstHeadingEl).tocId;\n        });\n\n        let duplicateTocId = false;\n        if (!tocId || otherTocIds.includes(tocId)) {\n            tocId = 1 + Math.max(0, ...otherTocIds);\n            duplicateTocId = true;\n        }\n\n        if (!headingHasChanged && areVisibilityIdsEqual && !duplicateTocId) {\n            return;\n        }\n\n        const headingIds = currentHeadingItems.map(({ el }) => getTocAndHeadingId(el).headingId);\n        let maxHeadingIds = Math.max(0, ...headingIds);\n\n        tableOfContentNavbar.textContent = \"\";\n        const uniqueHeadingIds = new Set();\n        for (const { title, el } of currentHeadingItems) {\n            let { headingId } = getTocAndHeadingId(el);\n            if (headingId) {\n                // Reset headingId on duplicate.\n                if (uniqueHeadingIds.has(headingId)) {\n                    headingId = 0;\n                } else {\n                    uniqueHeadingIds.add(headingId);\n                }\n            }\n            if (!headingId) {\n                maxHeadingIds += 1;\n                headingId = maxHeadingIds;\n            }\n            const tocHeadingId = `table_of_content_heading_${tocId}_${headingId}`;\n\n            const itemEl = this.document.createElement(\"a\");\n            itemEl.textContent = title;\n            itemEl.setAttribute(\"href\", `#${tocHeadingId}`);\n            itemEl.className =\n                \"table_of_content_link list-group-item list-group-item-action py-2 border-0 rounded-0\";\n            tableOfContentNavbar.appendChild(itemEl);\n\n            el.setAttribute(\"id\", tocHeadingId);\n        }\n    }\n}\n\nexport class NavbarPositionAction extends BuilderAction {\n    static id = \"navbarPosition\";\n    isApplied({ editingElement: navbarWrapEl, params: { mainParam: position } }) {\n        if (navbarWrapEl.classList.contains(\"s_table_of_content_horizontal_navbar\")) {\n            return position === \"top\";\n        } else {\n            const mainContent = navbarWrapEl.parentNode.querySelector(\".s_table_of_content_main\");\n            const previousSibling = navbarWrapEl.previousElementSibling;\n\n            return (previousSibling === mainContent ? \"right\" : \"left\") === position;\n        }\n    }\n    apply({ editingElement: navbarWrapEl, params: { mainParam: position } }) {\n        const mainContentEl = navbarWrapEl.parentElement.querySelector(\".s_table_of_content_main\");\n        const navbarEl = navbarWrapEl.querySelector(\".s_table_of_content_navbar\");\n\n        if (position === \"top\" || position === \"left\") {\n            const previousSibling = navbarWrapEl.previousElementSibling;\n            if (previousSibling) {\n                previousSibling.parentNode.insertBefore(navbarWrapEl, previousSibling);\n            }\n        }\n        if (position === \"left\" || position === \"right\") {\n            navbarWrapEl.classList.add(\"s_table_of_content_vertical_navbar\", \"col-lg-3\");\n            mainContentEl.classList.add(\"col-lg-9\");\n        }\n        if (position === \"right\") {\n            const nextSibling = navbarWrapEl.nextElementSibling;\n            if (nextSibling) {\n                nextSibling.parentNode.insertBefore(navbarWrapEl, nextSibling.nextSibling);\n            }\n        }\n        if (position === \"top\") {\n            navbarWrapEl.classList.add(\"s_table_of_content_horizontal_navbar\", \"col-lg-12\");\n            navbarEl.classList.add(\"list-group-horizontal-md\");\n            mainContentEl.classList.add(\"col-lg-12\");\n        }\n    }\n    clean({ editingElement: navbarWrapEl, params: { mainParam: position } }) {\n        const mainContentEl = navbarWrapEl.parentElement.querySelector(\".s_table_of_content_main\");\n        const navbarEl = navbarWrapEl.querySelector(\".s_table_of_content_navbar\");\n\n        if (position === \"top\") {\n            navbarWrapEl.classList.remove(\"s_table_of_content_horizontal_navbar\", \"col-lg-12\");\n            mainContentEl.classList.remove(\"col-lg-12\");\n            navbarEl.classList.remove(\"list-group-horizontal-md\");\n        }\n\n        if (position === \"left\" || position === \"right\") {\n            navbarWrapEl.classList.remove(\"s_table_of_content_vertical_navbar\", \"col-lg-3\");\n            mainContentEl.classList.remove(\"col-lg-9\");\n        }\n    }\n}\n\nregistry.category(\"website-plugins\").add(TableOfContentOptionPlugin.id, TableOfContentOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { applyFunDependOnSelectorAndExclude } from \"@html_builder/plugins/utils\";\n\nexport class TranslateTableOfContentOptionPlugin extends Plugin {\n    static id = \"tableOfContentOption\";\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        normalize_handlers: this.normalize.bind(this),\n        content_not_editable_selectors: [\".s_table_of_content_navbar\"],\n    };\n\n    normalize(root) {\n        applyFunDependOnSelectorAndExclude(this.updateTableOfContentNavbar.bind(this), root, {\n            selector: \".s_table_of_content_main\",\n        });\n    }\n\n    updateTableOfContentNavbar(tableOfContentMain) {\n        const tableOfContent = tableOfContentMain.closest(\".s_table_of_content\");\n        const tableOfContentNavbar = tableOfContent.querySelector(\".s_table_of_content_navbar\");\n        const currentNavbarItems = [...tableOfContentNavbar.children].map((el) => el.firstChild);\n\n        const targetedElements = \"h1, h2\";\n        const currentHeadingItems = [\n            ...tableOfContentMain.querySelectorAll(targetedElements),\n        ].filter((el) => !el.closest(\".o_snippet_desktop_invisible\"));\n\n        currentNavbarItems.map((el, i) => {\n            const newText = currentHeadingItems[i]?.textContent || \"\";\n            if (el.textContent !== newText) {\n                el.textContent = newText;\n            }\n\n            const newHref = `#${currentHeadingItems[i]?.id}`;\n            if (newHref && el.parentElement.getAttribute(\"href\") !== newHref) {\n                el.parentElement.setAttribute(\"href\", newHref);\n            }\n        });\n    }\n}\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { BEGIN, SNIPPET_SPECIFIC_END } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\n\nexport class TimelineListOption extends BaseOptionComponent {\n    static template = \"website.TimelineListOption\";\n    static selector = \".s_timeline_list\";\n}\n\nexport class DotLinesColorOption extends BaseOptionComponent {\n    static template = \"website.DotLinesColorOption\";\n    static selector = \".s_timeline_list\";\n}\n\nexport class DotColorOption extends BaseOptionComponent {\n    static template = \"website.DotColorOption\";\n    static selector = \".s_timeline_list .s_timeline_list_row\";\n}\n\nclass TimelineListOptionPlugin extends Plugin {\n    static id = \"timelineListOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            // TODO AGAU: alignment option sequence doesn't match master, must split template\n            withSequence(BEGIN, TimelineListOption),\n            withSequence(SNIPPET_SPECIFIC_END, DotLinesColorOption),\n            withSequence(BEGIN, DotColorOption),\n        ],\n        dropzone_selector: {\n            selector: \".s_timeline_list_row\",\n            dropNear: \".s_timeline_list_row\",\n        },\n        is_movable_selector: { selector: \".s_timeline_list_row\", direction: \"vertical\" },\n    };\n}\n\nregistry.category(\"website-plugins\").add(TimelineListOptionPlugin.id, TimelineListOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { registry } from \"@web/core/registry\";\nimport { after, before, SNIPPET_SPECIFIC_END } from \"@html_builder/utils/option_sequence\";\nimport { WEBSITE_BACKGROUND_OPTIONS } from \"@website/builder/option_sequence\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport const TIMELINE = before(WEBSITE_BACKGROUND_OPTIONS);\nexport const DOT_LINES_COLOR = SNIPPET_SPECIFIC_END;\nexport const DOT_COLOR = after(DOT_LINES_COLOR);\n\nfunction isTimelineCard(el) {\n    return el.matches(\".s_timeline_card\");\n}\n\nexport class TimelineOption extends BaseOptionComponent {\n    static template = \"website.TimelineOption\";\n    static selector = \".s_timeline\";\n}\n\nexport class DotLinesColorOption extends BaseOptionComponent {\n    static template = \"website.DotLinesColorOption\";\n    static selector = \".s_timeline\";\n}\n\nexport class DotColorOption extends BaseOptionComponent {\n    static template = \"website.DotColorOption\";\n    static selector = \".s_timeline .s_timeline_row\";\n}\n\nclass TimelineOptionPlugin extends Plugin {\n    static id = \"timelineOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(TIMELINE, TimelineOption),\n            withSequence(DOT_LINES_COLOR, DotLinesColorOption),\n            withSequence(DOT_COLOR, DotColorOption),\n        ],\n        dropzone_selector: {\n            selector: \".s_timeline_row\",\n            dropNear: \".s_timeline_row\",\n        },\n        has_overlay_options: { hasOption: (el) => isTimelineCard(el) },\n        get_overlay_buttons: withSequence(0, {\n            getButtons: this.getActiveOverlayButtons.bind(this),\n        }),\n        is_movable_selector: { selector: \".s_timeline_row\", direction: \"vertical\" },\n    };\n\n    setup() {\n        this.isEditableRTL = this.config.isEditableRTL;\n        this.isBackendRTL = localization.direction === \"rtl\";\n    }\n\n    getActiveOverlayButtons(target) {\n        if (!isTimelineCard(target)) {\n            this.overlayTarget = null;\n            return [];\n        }\n\n        this.overlayTarget = target;\n        const timelineRowEl = this.overlayTarget.closest(\".s_timeline_row\");\n        const firstContentEl = timelineRowEl.querySelector(\".s_timeline_content\");\n        const hasPreviousCard = !firstContentEl.contains(this.overlayTarget);\n        const reverseButtons = this.isEditableRTL !== this.isBackendRTL;\n        const direction = hasPreviousCard !== reverseButtons ? \"left\" : \"right\";\n        return [\n            {\n                class: `fa fa-fw fa-angle-${direction}`,\n                title: hasPreviousCard !== this.isEditableRTL ? _t(\"Move left\") : _t(\"Move right\"),\n                handler: this.moveTimelineCard.bind(this),\n            },\n        ];\n    }\n\n    moveTimelineCard() {\n        const timelineRowEl = this.overlayTarget.closest(\".s_timeline_row\");\n        const timelineCardEls = timelineRowEl.querySelectorAll(\".s_timeline_card\");\n        const firstContentEl = timelineRowEl.querySelector(\".s_timeline_content\");\n        timelineRowEl.append(firstContentEl);\n        timelineCardEls.forEach((card) => card.classList.toggle(\"text-md-end\"));\n    }\n}\n\nregistry.category(\"website-plugins\").add(TimelineOptionPlugin.id, TimelineOptionPlugin);\n", "export const CARD_PARENT_HANDLERS =\n    \".s_three_columns .row > div, .s_comparisons .row > div, .s_cards_grid .row > div, .s_cards_soft .row > div, .s_product_list .row > div, .s_newsletter_centered .row > div, .s_company_team_spotlight .row > div, .s_comparisons_horizontal .row > div, .s_company_team_grid .row > div, .s_company_team_card .row > div, .s_carousel_cards_item\";\n\n// To remove in master, move it on the Option\nexport const ONLY_BG_COLOR_SELECTOR =\n    \"section .row > div, .s_text_highlight, .s_mega_menu_thumbnails_footer, .s_hr, .s_cta_badge\";\nexport const ONLY_BG_COLOR_EXCLUDE = `.s_col_no_bgcolor, .s_col_no_bgcolor.row > div, .s_masonry_block .row > div, .s_color_blocks_2 .row > div, .s_image_gallery .row > div, .s_text_cover .row > .o_not_editable, [data-snippet] :not(.oe_structure) > .s_hr, ${CARD_PARENT_HANDLERS}, .s_website_form_cover .row > .o_not_editable, .s_bento_grid .row > div, .s_banner_categories .row > div`;\n\nexport const BASE_ONLY_BG_IMAGE_SELECTOR =\n    \".s_tabs .oe_structure > *, footer .oe_structure > *:not(.o_footer_bottom_part)\";\nexport const ONLY_BG_IMAGE_SELECTOR = BASE_ONLY_BG_IMAGE_SELECTOR;\nexport const ONLY_BG_IMAGE_EXCLUDE = \"\";\n\nexport const BOTH_BG_COLOR_IMAGE_SELECTOR =\n    \"section, .carousel-item, .s_masonry_block .row > div, .s_color_blocks_2 .row > div, .parallax, .s_text_cover .row > .o_not_editable, .s_website_form_cover .row > .o_not_editable, .s_split_intro .row > .o_not_editable, .s_bento_grid .row > div, .s_banner_categories .row > div, .s_ecomm_categories_showcase_block\";\nexport const BOTH_BG_COLOR_IMAGE_EXCLUDE = `${BASE_ONLY_BG_IMAGE_SELECTOR}, .s_carousel_wrapper, .s_image_gallery .carousel-item, .s_google_map, .s_map, [data-snippet] :not(.oe_structure) > [data-snippet], .s_masonry_block .s_col_no_resize, .s_quotes_carousel_wrapper, .s_carousel_intro_wrapper, .s_carousel_cards_item, .s_dynamic_snippet_category .s_dynamic_snippet_title`;\n\nexport const CARD_DISABLE_WIDTH_APPLY_TO = \":scope > .s_card:not(.s_carousel_cards_card)\";\nexport const WEBSITE_BG_APPLY_TO = \":scope > .s_carousel_cards_card\";\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class VisibilityOption extends BaseOptionComponent {\n    static template = \"website.VisibilityOption\";\n    static dependencies = [\"visibility\", \"websiteSession\"];\n    static selector = \"section, .s_hr\";\n\n    setup() {\n        super.setup();\n        this.websiteSession = this.dependencies.websiteSession.getSession();\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { pyToJsLocale } from \"@web/core/l10n/utils\";\nimport { VisibilityOption } from \"./visibility_option\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { CONDITIONAL_VISIBILITY, DEVICE_VISIBILITY } from \"@website/builder/option_sequence\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\n/**\n * @typedef {{\n *      saveAttribute: string;\n *      attributeName: string;\n *      callWith: \"code\" | \"name\" | \"value\" | \"id\";\n * }[]} visibility_selector_parameters\n */\nexport const DEVICE_VISIBILITY_OPTION_SELECTOR = \"section .row > div\";\n\nexport class DeviceVisibilityOption extends BaseOptionComponent {\n    static template = \"website.DeviceVisibilityOption\";\n    static dependencies = [\"visibility\"];\n    static selector = DEVICE_VISIBILITY_OPTION_SELECTOR;\n    static exclude = \".s_col_no_resize.row > div, .s_masonry_block .s_col_no_resize\";\n}\n\nclass VisibilityOptionPlugin extends Plugin {\n    static id = \"visibilityOption\";\n    static dependencies = [\"visibility\", \"websiteSession\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(CONDITIONAL_VISIBILITY, VisibilityOption),\n            withSequence(DEVICE_VISIBILITY, DeviceVisibilityOption),\n        ],\n        builder_actions: {\n            ForceVisibleAction,\n            ToggleDeviceVisibilityAction,\n        },\n        normalize_handlers: this.normalizeCSSSelectors.bind(this),\n        visibility_selector_parameters: [\n            {\n                saveAttribute: \"visibilityValueCountry\",\n                attributeName: \"data-country\",\n                callWith: \"code\",\n            },\n            {\n                saveAttribute: \"visibilityValueLang\",\n                attributeName: \"lang\",\n                callWith: \"code\",\n            },\n            {\n                saveAttribute: \"visibilityValueUtmCampaign\",\n                attributeName: \"data-utm-campaign\",\n                callWith: \"name\", // \"display_name\",\n            },\n            {\n                saveAttribute: \"visibilityValueUtmMedium\",\n                attributeName: \"data-utm-medium\",\n                callWith: \"name\", // \"display_name\",\n            },\n            {\n                saveAttribute: \"visibilityValueUtmSource\",\n                attributeName: \"data-utm-source\",\n                callWith: \"name\", // \"display_name\",\n            },\n            {\n                saveAttribute: \"visibilityValueLogged\",\n                attributeName: \"data-logged\",\n                callWith: \"value\",\n            },\n        ],\n    };\n\n    setup() {\n        this.optionsAttributes = this.getResource(\"visibility_selector_parameters\");\n    }\n\n    normalizeCSSSelectors(rootEl) {\n        for (const el of selectElements(rootEl, VisibilityOption.selector)) {\n            this.updateCSSSelectors(el);\n        }\n    }\n\n    /**\n     * Reads target's attributes and creates CSS selectors.\n     * Stores them in data-attributes to then be reapplied by\n     * content/inject_dom.js (ideally we should save them in a <style> tag\n     * directly but that would require a new website.page field and would not\n     * be possible in dynamic (controller) pages... maybe some day).\n     *\n     * @param {HTMLElement} target\n     */\n    updateCSSSelectors(target) {\n        if (target.dataset.visibility !== \"conditional\") {\n            // Cleanup on always visible\n            delete target.dataset.visibility;\n            for (const attribute of this.optionsAttributes) {\n                delete target.dataset[attribute.saveAttribute];\n                delete target.dataset[`${attribute.saveAttribute}Rule`];\n            }\n            delete target.dataset.visibilitySelectors;\n            delete target.dataset.visibilityId;\n            return;\n        }\n        // There are 2 data attributes per option:\n        // - One that stores the current records selected\n        // - Another that stores the value of the rule \"Hide for / Visible for\"\n        const visibilityIDParts = [];\n        const onlyAttributes = [];\n        const hideAttributes = [];\n        for (const attribute of this.optionsAttributes) {\n            if (target.dataset[attribute.saveAttribute]) {\n                let records = JSON.parse(target.dataset[attribute.saveAttribute]).map((record) => ({\n                    id: record.id,\n                    value: record[attribute.callWith],\n                }));\n                if (attribute.saveAttribute === \"visibilityValueLang\") {\n                    records = records.map((lang) => {\n                        lang.value = pyToJsLocale(lang.value);\n                        return lang;\n                    });\n                }\n                const hideFor = target.dataset[`${attribute.saveAttribute}Rule`] === \"hide\";\n                if (hideFor) {\n                    hideAttributes.push({ name: attribute.attributeName, records: records });\n                } else {\n                    onlyAttributes.push({ name: attribute.attributeName, records: records });\n                }\n                // Create a visibilityId based on the options name and their\n                // values. eg : hide for en_US(id:1) -> lang1h\n                const type = attribute.attributeName.replace(\"data-\", \"\");\n                const valueIDs = records.map((record) => record.id).sort();\n                visibilityIDParts.push(`${type}_${hideFor ? \"h\" : \"o\"}_${valueIDs.join(\"_\")}`);\n            }\n        }\n        const visibilityId = visibilityIDParts.join(\"_\");\n        // Creates CSS selectors based on those attributes, the reducers\n        // combine the attributes' values.\n        let selectors = \"\";\n        for (const attribute of onlyAttributes) {\n            // e.g of selector:\n            // html:not([data-attr-1=\"valueAttr1\"]):not([data-attr-1=\"valueAttr2\"]) [data-visibility-id=\"ruleId\"]\n            const selector =\n                attribute.records.reduce(\n                    (acc, record) => (acc += `:not([${attribute.name}=\"${record.value}\"])`),\n                    \"html\"\n                ) + ` body:not(.editor_enable) [data-visibility-id=\"${visibilityId}\"]`;\n            selectors += selector + \", \";\n        }\n        for (const attribute of hideAttributes) {\n            // html[data-attr-1=\"valueAttr1\"] [data-visibility-id=\"ruleId\"],\n            // html[data-attr-1=\"valueAttr2\"] [data-visibility-id=\"ruleId\"]\n            const selector = attribute.records.reduce((acc, record, i, a) => {\n                acc += `html[${attribute.name}=\"${record.value}\"] body:not(.editor_enable) [data-visibility-id=\"${visibilityId}\"]`;\n                return acc + (i !== a.length - 1 ? \",\" : \"\");\n            }, \"\");\n            selectors += selector + \", \";\n        }\n        selectors = selectors.slice(0, -2);\n        if (selectors) {\n            target.dataset.visibilitySelectors = selectors;\n        } else {\n            delete target.dataset.visibilitySelectors;\n        }\n\n        if (visibilityId) {\n            target.dataset.visibilityId = visibilityId;\n        } else {\n            delete target.dataset.visibilityId;\n        }\n    }\n}\n\nexport class ForceVisibleAction extends BuilderAction {\n    static id = \"forceVisible\";\n    static dependencies = [\"visibility\"];\n    apply({ editingElement }) {\n        this.dependencies.visibility.onOptionVisibilityUpdate(editingElement, true);\n    }\n    isApplied() {\n        return true;\n    }\n}\nexport class ToggleDeviceVisibilityAction extends BuilderAction {\n    static id = \"toggleDeviceVisibility\";\n    static dependencies = [\"visibility\", \"history\"];\n\n    apply({ editingElement, params: { mainParam: visibility } }) {\n        // Clean first as the widget is not part of a group\n        this.clean({ editingElement });\n        const style = getComputedStyle(editingElement);\n        if (visibility === \"no_desktop\") {\n            editingElement.classList.add(\"d-lg-none\", \"o_snippet_desktop_invisible\");\n        } else if (visibility === \"no_mobile\") {\n            editingElement.classList.add(\n                `d-lg-${style[\"display\"]}`,\n                \"d-none\",\n                \"o_snippet_mobile_invisible\"\n            );\n        }\n\n        // Update invisible elements\n        const isMobile = this.services.website.context.isMobile;\n        const show = visibility !== (isMobile ? \"no_mobile\" : \"no_desktop\");\n        this.dependencies.visibility.onOptionVisibilityUpdate(editingElement, show);\n        this.dependencies.history.applyCustomMutation({\n            apply: () => {},\n            revert: () => {\n                editingElement.classList.remove(\"o_snippet_override_invisible\");\n            },\n        });\n    }\n    clean({ editingElement }) {\n        editingElement.classList.remove(\n            \"d-none\",\n            \"d-md-none\",\n            \"d-lg-none\",\n            \"o_snippet_mobile_invisible\",\n            \"o_snippet_desktop_invisible\"\n        );\n        const style = getComputedStyle(editingElement);\n        const display = style[\"display\"];\n        editingElement.classList.remove(`d-md-${display}`, `d-lg-${display}`);\n        this.dependencies.history.applyCustomMutation({\n            apply: () => {\n                editingElement.classList.remove(\"o_snippet_override_invisible\");\n            },\n            revert: () => {},\n        });\n    }\n    isApplied({ editingElement, params: { mainParam: visibilityParam } }) {\n        const classList = [...editingElement.classList];\n        if (\n            visibilityParam === \"no_mobile\" &&\n            classList.includes(\"d-none\") &&\n            classList.some((className) => className.match(/^d-(md|lg)-/))\n        ) {\n            return true;\n        }\n        if (\n            visibilityParam === \"no_desktop\" &&\n            classList.some((className) => className.match(/d-(md|lg)-none/))\n        ) {\n            return true;\n        }\n        return false;\n    }\n}\n\nregistry.category(\"website-plugins\").add(VisibilityOptionPlugin.id, VisibilityOptionPlugin);\n", "import { BaseWebsiteBackgroundOption } from \"@website/builder/plugins/options/background_option\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport {\n    BOTH_BG_COLOR_IMAGE_EXCLUDE,\n    BOTH_BG_COLOR_IMAGE_SELECTOR,\n    ONLY_BG_COLOR_EXCLUDE,\n    ONLY_BG_COLOR_SELECTOR,\n    ONLY_BG_IMAGE_EXCLUDE,\n    ONLY_BG_IMAGE_SELECTOR,\n} from \"./utils\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { SNIPPET_SPECIFIC_BEFORE } from \"@html_builder/utils/option_sequence\";\nimport { WEBSITE_BACKGROUND_OPTIONS } from \"@website/builder/option_sequence\";\n\nexport class WebsiteBackgroundCarouselOption extends BaseWebsiteBackgroundOption {\n    static selector = \"section\";\n    static applyTo = \":scope > .carousel:not(.s_carousel_cards)\";\n    static defaultProps = {\n        withColors: true,\n        withImages: true,\n        withVideos: true,\n        withShapes: true,\n        withColorCombinations: true,\n    };\n}\n\nexport class WebsiteBackgroundBGColorImageOption extends BaseWebsiteBackgroundOption {\n    static selector = BOTH_BG_COLOR_IMAGE_SELECTOR;\n    static exclude = BOTH_BG_COLOR_IMAGE_EXCLUDE;\n    static defaultProps = {\n        withColors: true,\n        withImages: true,\n        withVideos: true,\n        withShapes: true,\n        withColorCombinations: true,\n    };\n}\nexport class WebsiteBackgroundBGColorOption extends BaseWebsiteBackgroundOption {\n    static selector = ONLY_BG_COLOR_SELECTOR;\n    static exclude = ONLY_BG_COLOR_EXCLUDE;\n    static defaultProps = {\n        withColors: true,\n        withImages: false,\n        withColorCombinations: true,\n    };\n}\nexport class WebsiteBackgroundOnlyBGImageOption extends BaseWebsiteBackgroundOption {\n    static selector = ONLY_BG_IMAGE_SELECTOR;\n    static exclude = ONLY_BG_IMAGE_EXCLUDE;\n    static defaultProps = {\n        withColors: false,\n        withImages: true,\n        withVideos: true,\n        withShapes: true,\n        withColorCombinations: false,\n    };\n}\n\nclass WebsiteBackgroundOptionPlugin extends Plugin {\n    static id = \"websiteOption\";\n    carouselApplyTo = \":scope > .carousel:not(.s_carousel_cards)\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(SNIPPET_SPECIFIC_BEFORE, WebsiteBackgroundCarouselOption),\n            withSequence(WEBSITE_BACKGROUND_OPTIONS, WebsiteBackgroundBGColorImageOption),\n            withSequence(WEBSITE_BACKGROUND_OPTIONS, WebsiteBackgroundBGColorOption),\n            withSequence(WEBSITE_BACKGROUND_OPTIONS, WebsiteBackgroundOnlyBGImageOption),\n        ],\n        mark_color_level_selector_params: [\n            {\n                selector: WebsiteBackgroundCarouselOption.selector,\n                applyTo: WebsiteBackgroundCarouselOption.applyTo,\n            },\n            {\n                selector: WebsiteBackgroundBGColorImageOption.selector,\n                exclude: WebsiteBackgroundBGColorImageOption.exclude,\n            },\n            {\n                selector: WebsiteBackgroundBGColorOption.selector,\n                exclude: WebsiteBackgroundBGColorOption.exclude,\n            },\n        ],\n    };\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(WebsiteBackgroundOptionPlugin.id, WebsiteBackgroundOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nexport class InfoPageOption extends BaseOptionComponent {\n    static template = \"website.InfoPageOption\";\n    static selector = \"main:has(.o_website_info)\";\n    static title = _t(\"Info Page\");\n    static editableOnly = false;\n    static groups = [\"website.group_website_designer\"];\n}\n\nclass WebsiteInfoPageOption extends Plugin {\n    static id = \"websiteInfoPageOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [InfoPageOption],\n    };\n}\n\nregistry.category(\"website-plugins\").add(WebsiteInfoPageOption.id, WebsiteInfoPageOption);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class TopMenuVisibilityOption extends BaseOptionComponent {\n    static template = \"website.TopMenuVisibilityOption\";\n    static dependencies = [\"websitePageConfigOptionPlugin\"];\n    static selector =\n        \"[data-main-object]:has(input.o_page_option_data[name='header_visible']) #wrapwrap > header\";\n    static groups = [\"website.group_website_designer\"];\n    static editableOnly = false;\n\n    setup() {\n        super.setup();\n        this.doesPageOptionExist =\n            this.dependencies.websitePageConfigOptionPlugin.doesPageOptionExist;\n    }\n}\n", "import { after } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { rgbaToHex } from \"@web/core/utils/colors\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { FOOTER_COPYRIGHT } from \"./footer_option_plugin\";\nimport { HEADER_TEMPLATE } from \"./header/header_option_plugin\";\nimport { TopMenuVisibilityOption } from \"./website_page_config_option\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\n/**\n * @typedef { Object } WebsitePageConfigOptionShared\n * @property { WebsitePageConfigOptionPlugin['setDirty'] } setDirty\n * @property { WebsitePageConfigOptionPlugin['setFooterVisible'] } setFooterVisible\n * @property { WebsitePageConfigOptionPlugin['getVisibilityItem'] } getVisibilityItem\n * @property { WebsitePageConfigOptionPlugin['getFooterVisibility'] } getFooterVisibility\n * @property { WebsitePageConfigOptionPlugin['doesPageOptionExist'] } doesPageOptionExist\n */\n\nexport const TOP_MENU_VISIBILITY = after(HEADER_TEMPLATE);\nexport const HIDE_FOOTER = after(FOOTER_COPYRIGHT);\n\nexport class HideFooterOption extends BaseOptionComponent {\n    static template = \"website.HideFooterOption\";\n    static selector =\n        \"[data-main-object]:has(input.o_page_option_data[name='footer_visible']) #wrapwrap > footer\";\n    static groups = [\"website.group_website_designer\"];\n    static editableOnly = false;\n}\n\nclass WebsitePageConfigOptionPlugin extends Plugin {\n    static id = \"websitePageConfigOptionPlugin\";\n    static dependencies = [\"history\", \"visibility\", \"builderActions\"];\n    static shared = [\n        \"setDirty\",\n        \"setFooterVisible\",\n        \"getVisibilityItem\",\n        \"getFooterVisibility\",\n        \"doesPageOptionExist\",\n    ];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_actions: {\n            SetWebsiteHeaderVisibilityAction,\n            SetWebsiteFooterVisibleAction,\n            SetPageWebsiteDirtyAction,\n        },\n        builder_options: [\n            withSequence(TOP_MENU_VISIBILITY, TopMenuVisibilityOption),\n            withSequence(HIDE_FOOTER, HideFooterOption),\n        ],\n        target_show: this.onTargetVisibilityToggle.bind(this, true),\n        target_hide: this.onTargetVisibilityToggle.bind(this, false),\n        save_handlers: this.onSave.bind(this),\n    };\n\n    getVisibilityItem() {\n        const isHidden = this.document\n            .querySelector(\"#wrapwrap > header\")\n            .classList.contains(\"o_snippet_invisible\");\n        const isOverlay = this.document\n            .getElementById(\"wrapwrap\")\n            .classList.contains(\"o_header_overlay\");\n        return isOverlay ? \"overTheContent\" : isHidden ? \"hidden\" : \"regular\";\n    }\n\n    getFooterVisibility() {\n        return this.document\n            .querySelector(\"#wrapwrap > footer\")\n            .classList.contains(\"o_snippet_invisible\");\n    }\n\n    getColorValue(attribute, classPrefix) {\n        const headerEl = this.document.querySelector(\"#wrapwrap > header\");\n        const matchingClass = [...headerEl.classList].find((cls) => cls.startsWith(classPrefix));\n        return matchingClass || rgbaToHex(headerEl.style.getPropertyValue(attribute));\n    }\n\n    setDirty(isPreviewing) {\n        if (isPreviewing) {\n            return;\n        }\n        this.isDirty = true;\n    }\n\n    onSave() {\n        if (!this.isDirty) {\n            return;\n        }\n        const item = this.getVisibilityItem();\n        const pageOptions = {\n            header_overlay: () => item === \"overTheContent\",\n            header_color: () => this.getColorValue(\"background-color\", \"bg-\"),\n            header_text_color: () => this.getColorValue(\"color\", \"text-\"),\n            header_visible: () => item !== \"hidden\",\n            footer_visible: () => !this.getFooterVisibility(),\n        };\n\n        const args = {};\n        for (const [pageOptionName, valueGetter] of Object.entries(pageOptions)) {\n            if (this.doesPageOptionExist(pageOptionName)) {\n                args[pageOptionName] = valueGetter();\n            }\n        }\n\n        const mainObject = this.services.website.currentWebsite.metadata.mainObject;\n        return Promise.all([this.services.orm.write(mainObject.model, [mainObject.id], args)]);\n    }\n\n    doesPageOptionExist(pageOptionName) {\n        return this.document.querySelector(\n            `[data-main-object]:has(input.o_page_option_data[name='${pageOptionName}'])`\n        );\n    }\n\n    setFooterVisible(show) {\n        const footerEl = this.document.querySelector(\"#wrapwrap > footer\");\n        footerEl.classList.toggle(\"d-none\", !show);\n        footerEl.classList.toggle(\"o_snippet_invisible\", !show);\n        this.dependencies.visibility.onOptionVisibilityUpdate(footerEl, show);\n    }\n\n    onTargetVisibilityToggle(show, target) {\n        if (show && target.matches(\"#wrapwrap > header\")) {\n            this.dependencies.builderActions.applyAction(\"setWebsiteHeaderVisibility\", {\n                editingElement: target,\n                value: \"regular\",\n                isPreviewing: false,\n            });\n        }\n        if (show && target.matches(\"#wrapwrap > footer\")) {\n            this.dependencies.builderActions.applyAction(\"setWebsiteFooterVisible\", {\n                editingElement: target,\n                isPreviewing: false,\n            });\n        }\n    }\n}\nexport class BaseWebsitePageConfigAction extends BuilderAction {\n    static id = \"baseWebsitePageConfig\";\n    static dependencies = [\"websitePageConfigOptionPlugin\", \"history\", \"visibility\"];\n    setup() {\n        this.websitePageConfig = this.dependencies.websitePageConfigOptionPlugin;\n        this.visibility = this.dependencies.visibility;\n        this.history = this.dependencies.history;\n        this.visibilityHandlers = {\n            overTheContent: () => {\n                this.setHeaderOverlay(true);\n                this.setHeaderVisible(false);\n            },\n            regular: () => {\n                this.setHeaderOverlay(false);\n                this.setHeaderVisible(false);\n                this.resetHeaderColor();\n                this.resetTextColor();\n            },\n            hidden: () => {\n                this.setHeaderOverlay(false);\n                this.setHeaderVisible(true);\n                this.resetHeaderColor();\n                this.resetTextColor();\n            },\n        };\n    }\n\n    setHeaderOverlay(shouldApply) {\n        this.document.getElementById(\"wrapwrap\").classList.toggle(\"o_header_overlay\", shouldApply);\n    }\n\n    setHeaderVisible(shouldApply) {\n        const headerEl = this.document.querySelector(\"#wrapwrap > header\");\n        headerEl.classList.toggle(\"d-none\", shouldApply);\n        headerEl.classList.toggle(\"o_snippet_invisible\", shouldApply);\n        this.visibility.onOptionVisibilityUpdate(headerEl, !shouldApply);\n    }\n\n    resetHeaderColor() {\n        const headerEl = this.document.querySelector(\"#wrapwrap > header\");\n        headerEl.style.removeProperty(\"background-color\");\n        headerEl.classList.forEach((cls) => {\n            if (cls.startsWith(\"bg-o-color-\")) {\n                headerEl.classList.remove(cls);\n            }\n        });\n    }\n\n    resetTextColor() {\n        const headerEl = this.document.querySelector(\"#wrapwrap > header\");\n        headerEl.style.removeProperty(\"color\");\n        headerEl.classList.forEach((cls) => {\n            if (cls.startsWith(\"text-o-color-\")) {\n                headerEl.classList.remove(cls);\n            }\n        });\n    }\n}\nexport class SetWebsiteHeaderVisibilityAction extends BaseWebsitePageConfigAction {\n    static id = \"setWebsiteHeaderVisibility\";\n    apply({ editingElement, value: headerPositionValue, isPreviewing }) {\n        const lastValue = this.websitePageConfig.getVisibilityItem();\n        this.history.applyCustomMutation({\n            apply: () => this.visibilityHandlers[headerPositionValue](),\n            revert: () => this.visibilityHandlers[lastValue](),\n        });\n\n        this.websitePageConfig.setDirty(isPreviewing);\n    }\n    isApplied({ editingElement, value }) {\n        return this.websitePageConfig.getVisibilityItem() === value;\n    }\n}\nexport class SetWebsiteFooterVisibleAction extends BaseWebsitePageConfigAction {\n    static id = \"setWebsiteFooterVisible\";\n    isApplied({ editingElement }) {\n        return !this.websitePageConfig.getFooterVisibility();\n    }\n    apply({ editingElement, isPreviewing }) {\n        this.websitePageConfig.setFooterVisible(true);\n        this.websitePageConfig.setDirty(isPreviewing);\n    }\n    clean({ editingElement, isPreviewing }) {\n        this.websitePageConfig.setFooterVisible(false);\n        this.websitePageConfig.setDirty(isPreviewing);\n    }\n}\n\nexport class SetPageWebsiteDirtyAction extends BaseWebsitePageConfigAction {\n    static id = \"setPageWebsiteDirty\";\n    apply({ editingElement, isPreviewing }) {\n        this.websitePageConfig.setDirty(isPreviewing);\n    }\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(WebsitePageConfigOptionPlugin.id, WebsitePageConfigOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { patch } from \"@web/core/utils/patch\";\n\n/**\n * @typedef { Object } PopupVisibilityShared\n * @property { PopupVisibilityPlugin['onTargetHide'] } onTargetHide\n * @property { PopupVisibilityPlugin['onTargetShow'] } onTargetShow\n */\n\nexport class PopupVisibilityPlugin extends Plugin {\n    static id = \"popupVisibilityPlugin\";\n    static dependencies = [\"visibility\", \"history\"];\n    static shared = [\"onTargetShow\", \"onTargetHide\"];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        target_show: this.onTargetShow.bind(this),\n        target_hide: this.onTargetHide.bind(this),\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        on_restore_containers_handlers: this.hidePopupsWithoutTarget.bind(this),\n        on_reveal_target_handlers: this.hidePopupsWithoutTarget.bind(this),\n    };\n\n    setup() {\n        this.addDomListener(this.editable, \"click\", (ev) => {\n            // Note: links are excluded here so that internal modal buttons do\n            // not close the popup as we want to allow edition of those buttons.\n            if (ev.target.matches(\".s_popup .js_close_popup:not(a, .btn)\")) {\n                ev.stopPropagation();\n                const popupEl = ev.target.closest(\".s_popup\");\n                this.dependencies.visibility.hideElement(popupEl);\n            }\n        });\n        const history = this.dependencies.history;\n        this.unpatchModal = this.window.Modal // null in tests without loadAssetsFrontendJS\n            ? patch(this.window.Modal.prototype, {\n                  _hideModal() {\n                      return history.ignoreDOMMutations(() => super._hideModal());\n                  },\n                  show() {\n                      return history.ignoreDOMMutations(() => super.show());\n                  },\n                  hide() {\n                      return history.ignoreDOMMutations(() => super.hide());\n                  },\n              })\n            : () => {};\n    }\n\n    destroy() {\n        super.destroy();\n        this.unpatchModal();\n    }\n\n    onTargetShow(targetEl) {\n        // Check if the popup is within the editable, because it is cloned on\n        // save (see save plugin) and Bootstrap moves it if it is not within the\n        // document (see Bootstrap Modal's _showElement).\n        if (targetEl.matches(\".s_popup\") && this.editable.contains(targetEl)) {\n            this.window.Modal.getOrCreateInstance(targetEl.querySelector(\".modal\")).show();\n        }\n    }\n\n    onTargetHide(targetEl, isCleaning) {\n        // Do not use Bootstrap to close the popup, as we are cleaning a\n        // clone of it. Instead, hide it manually (see `cleanForSave`).\n        if (targetEl.matches(\".s_popup\") && !isCleaning) {\n            this.window.Modal.getOrCreateInstance(targetEl.querySelector(\".modal\")).hide();\n        }\n    }\n\n    cleanForSave({ root: rootEl }) {\n        // Hide the popups manually, as we cannot rely on the `onTargetHide`\n        // flow since the cleaned popup is a clone and is not in the DOM.\n        for (const modalEl of rootEl.querySelectorAll(\".s_popup .modal.show\")) {\n            modalEl.parentElement.dataset.invisible = \"1\";\n            // Do not call .hide() directly, because it is queued whereas\n            // .dispose() is not.\n            modalEl.classList.remove(\"show\");\n            this.window.Modal.getOrCreateInstance(modalEl)._hideModal();\n            this.window.Modal.getInstance(modalEl).dispose();\n        }\n    }\n\n    /**\n     * Hides all the open popups that do not contain the given target element.\n     *\n     * @param {HTMLElement} targetEl the element\n     */\n    hidePopupsWithoutTarget(targetEl) {\n        const openPopupEls = this.editable.querySelectorAll(\".s_popup:not([data-invisible='1']\");\n        if (!openPopupEls.length) {\n            return;\n        }\n\n        for (const popupEl of openPopupEls) {\n            if (!popupEl.contains(targetEl)) {\n                this.dependencies.visibility.toggleTargetVisibility(popupEl, false);\n            }\n        }\n        this.config.updateInvisibleElementsPanel();\n    }\n}\n\nregistry.category(\"website-plugins\").add(PopupVisibilityPlugin.id, PopupVisibilityPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\n\nexport class RemoveTranslationBrandingPlugin extends Plugin {\n    static id = \"removeTranslationBrandingPlugin\";\n\n    setup() {\n        // Cleaning translation branding nodes that could have been saved\n        // by error between with code between d4d428ff1d (29th October 2025) and\n        // f09dc4d9d3 (19th November 2025)\n        this.editable\n            .querySelectorAll(\"span[data-oe-model][data-oe-translation-source-sha]\")\n            .forEach((brandingElement) => {\n                brandingElement.replaceWith(...brandingElement.childNodes);\n            });\n    }\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(RemoveTranslationBrandingPlugin.id, RemoveTranslationBrandingPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nexport class SaveTranslationPlugin extends Plugin {\n    static id = \"saveTranslation\";\n    static dependencies = [\"savePlugin\"];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        save_elements_overrides: withSequence(20, this.saveTranslationElements.bind(this)),\n    };\n\n    /**\n     * If the elements hold a translation, saves it. Otherwise, fallback to the\n     * standard saving with the lang kept.\n     *\n     * @param {Array<HTMLElement>} els - the elements to save.\n     */\n    async saveTranslationElements(els) {\n        if (els[0].dataset[\"oeTranslationSourceSha\"]) {\n            const translations = {};\n            translations[this.services.website.currentWebsite.metadata.lang] = Object.assign(\n                {},\n                ...els.map((el) => ({\n                    [el.dataset[\"oeTranslationSourceSha\"]]: this.getEscapedElement(el).innerHTML,\n                }))\n            );\n            return rpc(\"/website/field/translation/update\", {\n                model: els[0].dataset[\"oeModel\"],\n                record_id: [Number(els[0].dataset[\"oeId\"])],\n                field_name: els[0].dataset[\"oeField\"],\n                translations,\n            });\n        }\n        await this.dependencies.savePlugin.saveView(els[0], false);\n        return true;\n    }\n\n    getEscapedElement(el) {\n        const escapedEl = el.cloneNode(true);\n        const allElements = [escapedEl, ...escapedEl.querySelectorAll(\"*\")];\n        const exclusion = [];\n        for (const element of allElements) {\n            if (\n                element.matches(\n                    \"object,iframe,script,style,[data-oe-model]:not([data-oe-model='ir.ui.view'])\"\n                )\n            ) {\n                exclusion.push(element);\n                exclusion.push(...element.querySelectorAll(\"*\"));\n            }\n        }\n        const exclusionSet = new Set(exclusion);\n        const toEscapeEls = allElements.filter((el) => !exclusionSet.has(el));\n        for (const toEscapeEl of toEscapeEls) {\n            for (const child of Array.from(toEscapeEl.childNodes)) {\n                if (child.nodeType === 3) {\n                    const divEl = document.createElement(\"div\");\n                    divEl.textContent = child.nodeValue;\n                    child.nodeValue = divEl.innerHTML;\n                }\n            }\n        }\n        return escapedEl;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\n\nexport class WebsiteSetupEditorPlugin extends Plugin {\n    static id = \"website.setup_editor_plugin\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        snippet_preview_dialog_bundles: [\"web.assets_frontend\"],\n    };\n}\n\nregistry.category(\"website-plugins\").add(WebsiteSetupEditorPlugin.id, WebsiteSetupEditorPlugin);\n", "import { after, BLOCK_ALIGN } from \"@html_builder/utils/option_sequence\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class SizeOption extends BaseOptionComponent {\n    static template = \"website.SizeOption\";\n    static selector = \".s_alert\";\n}\n\nclass SizeOptionPlugin extends Plugin {\n    static id = \"sizeOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(after(BLOCK_ALIGN), SizeOption)],\n    };\n}\nregistry.category(\"website-plugins\").add(SizeOptionPlugin.id, SizeOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nclass SnippetsPowerboxPlugin extends Plugin {\n    static id = \"alert\";\n    static dependencies = [\"dom\", \"history\"];\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"s_alert\",\n                title: _t(\"Alert\"),\n                description: _t(\"Insert an alert snippet\"),\n                icon: \"fa-info\",\n                run: this.insertSnippet.bind(this, \"s_alert\"),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"s_rating\",\n                title: _t(\"Rating\"),\n                description: _t(\"Insert a rating snippet\"),\n                icon: \"fa-star-half-o\",\n                run: this.insertSnippet.bind(this, \"s_rating\"),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"s_card\",\n                title: _t(\"Card\"),\n                description: _t(\"Insert a card snippet\"),\n                icon: \"fa-sticky-note\",\n                run: this.insertSnippet.bind(this, \"s_card\"),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"s_share\",\n                title: _t(\"Share\"),\n                description: _t(\"Insert a share snippet\"),\n                icon: \"fa-share-square-o\",\n                run: this.insertSnippet.bind(this, \"s_share\"),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"s_text_highlight\",\n                title: _t(\"Text Highlight\"),\n                description: _t(\"Insert a text highlight snippet\"),\n                icon: \"fa-sticky-note\",\n                run: this.insertSnippet.bind(this, \"s_text_highlight\"),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"s_chart\",\n                title: _t(\"Chart\"),\n                description: _t(\"Insert a chart snippet\"),\n                icon: \"fa-bar-chart\",\n                run: this.insertSnippet.bind(this, \"s_chart\"),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"s_progress_bar\",\n                title: _t(\"Progress Bar\"),\n                description: _t(\"Insert a progress bar snippet\"),\n                icon: \"fa-spinner\",\n                run: this.insertSnippet.bind(this, \"s_progress_bar\"),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"s_badge\",\n                title: _t(\"Badge\"),\n                description: _t(\"Insert a badge snippet\"),\n                icon: \"fa-tags\",\n                run: this.insertSnippet.bind(this, \"s_badge\"),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"s_blockquote\",\n                title: _t(\"Blockquote\"),\n                description: _t(\"Insert a blockquote snippet\"),\n                icon: \"fa-quote-left\",\n                run: this.insertSnippet.bind(this, \"s_blockquote\"),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"s_hr\",\n                title: _t(\"Separator\"),\n                description: _t(\"Insert a horizontal separator snippet\"),\n                icon: \"fa-minus\",\n                run: this.insertSnippet.bind(this, \"s_hr\"),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        powerbox_categories: withSequence(110, {\n            id: \"website\",\n            name: _t(\"Website\"),\n        }),\n        powerbox_items: [\n            {\n                categoryId: \"website\",\n                commandId: \"s_alert\",\n            },\n            {\n                categoryId: \"website\",\n                commandId: \"s_rating\",\n            },\n            {\n                categoryId: \"website\",\n                commandId: \"s_card\",\n            },\n            {\n                categoryId: \"website\",\n                commandId: \"s_share\",\n            },\n            {\n                categoryId: \"website\",\n                commandId: \"s_text_highlight\",\n            },\n            {\n                categoryId: \"website\",\n                commandId: \"s_chart\",\n            },\n            {\n                categoryId: \"website\",\n                commandId: \"s_progress_bar\",\n            },\n            {\n                categoryId: \"website\",\n                commandId: \"s_badge\",\n            },\n            {\n                categoryId: \"website\",\n                commandId: \"s_blockquote\",\n            },\n            {\n                categoryId: \"website\",\n                commandId: \"s_hr\",\n            },\n        ],\n    };\n\n    insertSnippet(name) {\n        const snippet = this.config.snippetModel.getSnippetByName(\"snippet_content\", name);\n        const content = snippet.content.cloneNode(true);\n        this.dependencies.dom.insert(content);\n        this.dependencies.history.addStep();\n    }\n}\n\nregistry.category(\"website-plugins\").add(SnippetsPowerboxPlugin.id, SnippetsPowerboxPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { onWillStart } from \"@odoo/owl\";\n\nexport class SwitchableViews extends BaseOptionComponent {\n    static template = \"website.SwitchableViews\";\n    static dependencies = [\"switchableViews\"];\n    static selector = \".o_portal_wrap\";\n    static groups = [\"website.group_website_designer\"];\n    static editableOnly = false;\n\n    setup() {\n        super.setup();\n        const { getSwitchableRelatedViews } = this.dependencies.switchableViews;\n        onWillStart(async () => {\n            this.switchableRelatedViews = await getSwitchableRelatedViews();\n        });\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { SwitchableViews } from \"./switchable_views\";\n\n/**\n * @typedef { Object } SwitchableViewsShared\n * @property { SwitchableViewsPlugin['getSwitchableRelatedViews'] } getSwitchableRelatedViews\n */\n\nexport class SwitchableViewsPlugin extends Plugin {\n    static id = \"switchableViews\";\n    static dependencies = [\"customizeWebsite\"];\n    static shared = [\"getSwitchableRelatedViews\"];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [SwitchableViews],\n    };\n\n    /**\n     * @returns {Promise<[]>}\n     */\n    getSwitchableRelatedViews() {\n        if (!this.prom) {\n            const viewKey = this.document.querySelector(\"html\").dataset.viewXmlid;\n            if (this.services.website.isDesigner && viewKey) {\n                this.prom = rpc(\"/website/get_switchable_related_views\", {\n                    key: viewKey,\n                });\n                this.prom.then((views) => {\n                    for (const view of views) {\n                        const promise = Promise.resolve(view.active);\n                        this.dependencies.customizeWebsite.populateCache(view.key, promise);\n                    }\n                });\n            } else {\n                this.prom = Promise.resolve([]);\n            }\n        }\n        return this.prom;\n    }\n}\n\nregistry.category(\"website-plugins\").add(SwitchableViewsPlugin.id, SwitchableViewsPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ThemeAdvancedOption extends BaseOptionComponent {\n    static template = \"website.ThemeAdvancedOption\";\n    static dependencies = [\"themeTab\"];\n    setup() {\n        super.setup();\n        this.grays = useState(this.dependencies.themeTab.getGrays());\n    }\n\n    getGrayTitle(grayCode) {\n        return _t(\"Gray %(grayCode)s\", { grayCode });\n    }\n}\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { ThemeFontFamilyOption } from \"./theme_fontfamily_option\";\n\nexport class ThemeButtonOption extends BaseOptionComponent {\n    static template = \"website.ThemeButtonOption\";\n    static components = {\n        ThemeFontFamilyOption,\n    };\n}\n", "import { onMounted } from \"@odoo/owl\";\nimport { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { getCSSVariableValue } from \"@html_editor/utils/formatting\";\n\nexport class ThemeColorsOption extends BaseOptionComponent {\n    static template = \"website.ThemeColorsOption\";\n    setup() {\n        super.setup();\n        this.palettes = this.getPalettes();\n        this.colorPresetToShow = this.env.colorPresetToShow;\n        this.state = useDomState(() => ({\n            presets: this.getPresets(),\n        }));\n        onMounted(() => {\n            this.iframeDocument = document.querySelector(\"iframe\").contentWindow.document;\n            this.state.presets = this.getPresets();\n            this.colorPresetToShow = null;\n        });\n    }\n\n    getPalettes() {\n        const palettes = [];\n        const style = window.getComputedStyle(document.documentElement);\n        const allPaletteNames = getCSSVariableValue(\"palette-names\", style)\n            .split(\", \")\n            .map((name) => name.replace(/'/g, \"\"));\n        for (const paletteName of allPaletteNames) {\n            const palette = {\n                name: paletteName,\n                colors: [],\n            };\n            [1, 3, 2].forEach((c) => {\n                const color = getCSSVariableValue(`o-palette-${paletteName}-o-color-${c}`, style);\n                palette.colors.push(color);\n            });\n            palettes.push(palette);\n        }\n        return palettes;\n    }\n\n    getPresets() {\n        const presets = [];\n        const unquote = (string) => string.substring(1, string.length - 1);\n        for (let i = 1; i <= 5; i++) {\n            const preset = {\n                id: i,\n                background: this.getColor(`o-cc${i}-bg`),\n                backgroundGradient: unquote(this.getColor(`o-cc${i}-bg-gradient`)),\n                text: this.getColor(`o-cc${i}-text`),\n                headings: this.getColor(`o-cc${i}-headings`),\n                primaryBtn: this.getColor(`o-cc${i}-btn-primary`),\n                primaryBtnText: this.getColor(`o-cc${i}-btn-primary-text`),\n                primaryBtnBorder: this.getColor(`o-cc${i}-btn-primary-border`),\n                secondaryBtn: this.getColor(`o-cc${i}-btn-secondary`),\n                secondaryBtnText: this.getColor(`o-cc${i}-btn-secondary-text`),\n                secondaryBtnBorder: this.getColor(`o-cc${i}-btn-secondary-border`),\n            };\n\n            // TODO: check if this is necessary\n            if (preset.backgroundGradient) {\n                preset.backgroundGradient += \", url('/web/static/img/transparent.png')\";\n            }\n            presets.push(preset);\n        }\n        return presets;\n    }\n\n    getColor(color) {\n        if (!this.iframeDocument) {\n            return \"\";\n        }\n        if (!this.iframeStyle) {\n            this.iframeStyle = this.iframeDocument.defaultView.getComputedStyle(\n                this.iframeDocument.documentElement\n            );\n        }\n        return getCSSVariableValue(color, this.iframeStyle);\n    }\n}\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { BuilderFontFamilyPicker } from \"@html_builder/core/building_blocks/builder_fontfamilypicker\";\nimport { BuilderButton } from \"@html_builder/core/building_blocks/builder_button\";\nimport { getCSSVariableValue } from \"@html_editor/utils/formatting\";\n\nexport class ThemeFontFamilyOption extends BaseOptionComponent {\n    static template = \"html_builder.ThemeFontFamilyOption\";\n    static props = {\n        cssVariable: String,\n        buttonIcon: String,\n        buttonTitle: String,\n    };\n    static components = {\n        BuilderFontFamilyPicker,\n        BuilderButton,\n    };\n\n    setup() {\n        super.setup();\n        const htmlStyle = this.env.editor.document.defaultView.getComputedStyle(\n            this.env.getEditingElement()\n        );\n        if (this.props.cssVariable === \"headings-font\") {\n            this.state = useDomState(() => ({\n                isFontSpecified:\n                    getCSSVariableValue(\"headings-font\", htmlStyle) !==\n                    getCSSVariableValue(\"default-headings-font\", htmlStyle),\n            }));\n        } else {\n            this.state = useDomState(() => ({\n                isFontSpecified: !!getCSSVariableValue(\"set-\" + this.props.cssVariable, htmlStyle),\n            }));\n        }\n    }\n}\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { ThemeFontFamilyOption } from \"./theme_fontfamily_option\";\n\nexport class ThemeHeadingsOption extends BaseOptionComponent {\n    static template = \"website.ThemeHeadingsOption\";\n    static components = {\n        ThemeFontFamilyOption,\n    };\n}\n", "import { Component, useState, useSubEnv } from \"@odoo/owl\";\nimport { OptionsContainer } from \"@html_builder/sidebar/option_container\";\nimport { useOptionsSubEnv } from \"@html_builder/utils/utils\";\n\nexport class ThemeTab extends Component {\n    static template = \"website.ThemeTab\";\n    static components = { OptionsContainer };\n    static props = {\n        // optionsContainers: { type: Array, optional: true },\n        colorPresetToShow: { type: Number | null, optional: true },\n    };\n    static defaultProps = {\n        // optionsContainers: [],\n    };\n\n    setup() {\n        useOptionsSubEnv(() => [this.env.editor.document.body]);\n        useSubEnv({\n            colorPresetToShow: this.props.colorPresetToShow,\n        });\n        this.state = useState({\n            fontsData: {},\n        });\n        this.optionsContainers = this.env.editor.resources[\"theme_options\"];\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { getCSSVariableValue, getHtmlStyle } from \"@html_editor/utils/formatting\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { ThemeAdvancedOption } from \"./theme_advanced_option\";\nimport { ThemeButtonOption } from \"./theme_button_option\";\nimport { ThemeColorsOption } from \"./theme_colors_option\";\nimport { ThemeHeadingsOption } from \"./theme_headings_option\";\nimport { setBuilderCSSVariables } from \"@html_builder/utils/utils_css\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport {\n    convertCSSColorToRgba,\n    convertRgbaToCSSColor,\n    convertHslToRgb,\n    convertRgbToHsl,\n} from \"@web/core/utils/colors\";\nimport { reactive } from \"@odoo/owl\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { CustomizeWebsiteVariableAction } from \"../customize_website_plugin\";\nimport { EditHeadBodyDialog } from \"@website/components/edit_head_body_dialog/edit_head_body_dialog\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\n/**\n * @typedef { Object } ThemeTabShared\n * @property { ThemeTabPlugin['buildGray'] } buildGray\n * @property { ThemeTabPlugin['getGrays'] } getGrays\n * @property { ThemeTabPlugin['getGrayParams'] } getGrayParams\n * @property { ThemeTabPlugin['setGrays'] } setGrays\n * @property { ThemeTabPlugin['setGrayParams'] } setGrayParams\n */\n\n/**\n * @typedef {import(\"@html_builder/core/builder_options_plugin\").BuilderOptionContainer[]} theme_options\n */\n\nexport const GRAY_PARAMS = {\n    EXTRA_SATURATION: \"gray-extra-saturation\",\n    HUE: \"gray-hue\",\n};\n\nexport const OPTION_POSITIONS = {\n    COLORS: 10,\n    SETTINGS: 20,\n    PARAGRAPH: 30,\n    HEADINGS: 40,\n    BUTTON: 50,\n    LINK: 60,\n    INPUT: 70,\n    ADVANCED: 80,\n};\n\nexport class ThemeTabPlugin extends Plugin {\n    static id = \"themeTab\";\n    static shared = [\"getGrayParams\", \"getGrays\", \"setGrays\", \"setGrayParams\", \"buildGray\"];\n    grayParams = {};\n    grays = reactive({});\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_actions: {\n            CustomizeGrayAction,\n            ChangeColorPaletteAction,\n            EditCustomCodeAction,\n            ConfigureApiKeyAction,\n            CustomizePageLayout,\n        },\n        theme_options: [\n            withSequence(\n                OPTION_POSITIONS.COLORS,\n                this.getThemeOptionBlock(\"theme-colors\", _t(\"Colors\"), ThemeColorsOption)\n            ),\n            withSequence(\n                OPTION_POSITIONS.SETTINGS,\n                this.getThemeOptionBlock(\n                    \"website-settings\",\n                    _t(\"Website\"),\n                    class ThemeWebsiteSettingsOption extends BaseOptionComponent {\n                        static template = \"website.ThemeWebsiteSettingsOption\";\n                    }\n                )\n            ),\n            withSequence(\n                OPTION_POSITIONS.PARAGRAPH,\n                this.getThemeOptionBlock(\n                    \"theme-paragraph\",\n                    _t(\"Paragraph\"),\n                    class ThemeParagraphOption extends BaseOptionComponent {\n                        static template = \"website.ThemeParagraphOption\";\n                    }\n                )\n            ),\n            withSequence(\n                OPTION_POSITIONS.HEADINGS,\n                this.getThemeOptionBlock(\"theme-headings\", _t(\"Headings\"), ThemeHeadingsOption)\n            ),\n            withSequence(\n                OPTION_POSITIONS.BUTTON,\n                this.getThemeOptionBlock(\"theme-button\", _t(\"Button\"), ThemeButtonOption)\n            ),\n            withSequence(\n                OPTION_POSITIONS.LINK,\n                this.getThemeOptionBlock(\n                    \"theme-link\",\n                    _t(\"Link\"),\n                    class ThemeLinkOption extends BaseOptionComponent {\n                        static template = \"website.ThemeLinkOption\";\n                    }\n                )\n            ),\n            withSequence(\n                OPTION_POSITIONS.INPUT,\n                this.getThemeOptionBlock(\n                    \"theme-input\",\n                    _t(\"Input Fields\"),\n                    class ThemeInputOption extends BaseOptionComponent {\n                        static template = \"website.ThemeInputOption\";\n                    }\n                )\n            ),\n            withSequence(\n                OPTION_POSITIONS.ADVANCED,\n                this.getThemeOptionBlock(\"theme-advanced\", _t(\"Advanced\"), ThemeAdvancedOption)\n            ),\n        ],\n    };\n\n    setup() {\n        // If the gray palette has been generated by Odoo standard option,\n        // the hue of all gray is the same and the saturation has been\n        // increased/decreased by the same amount for all grays in\n        // comparaison with BS grays. However the system supports any\n        // gray palette.\n\n        const hues = [];\n        const saturationDiffs = [];\n        let oneHasNoSaturation = false;\n        const style = this.window.getComputedStyle(this.document.body);\n        const baseStyle = getComputedStyle(document.body);\n        for (let id = 100; id <= 900; id += 100) {\n            const gray = getCSSVariableValue(`${id}`, style);\n            this.grays[id] = gray;\n            const grayRGB = convertCSSColorToRgba(gray);\n            const grayHSL = convertRgbToHsl(grayRGB.red, grayRGB.green, grayRGB.blue);\n\n            const baseGray = getCSSVariableValue(`base-${id}`, baseStyle);\n            const baseGrayRGB = convertCSSColorToRgba(baseGray);\n            const baseGrayHSL = convertRgbToHsl(\n                baseGrayRGB.red,\n                baseGrayRGB.green,\n                baseGrayRGB.blue\n            );\n\n            if (grayHSL.saturation > 0.01) {\n                if (grayHSL.lightness > 0.01 && grayHSL.lightness < 99.99) {\n                    hues.push(grayHSL.hue);\n                }\n                if (grayHSL.saturation < 99.99) {\n                    saturationDiffs.push(grayHSL.saturation - baseGrayHSL.saturation);\n                }\n            } else {\n                oneHasNoSaturation = true;\n            }\n        }\n        this.grayHueIsDefined = !!hues.length;\n\n        // Average of angles: we need to take the average of found hues\n        // because even if grays are supposed to be set to the exact\n        // same hue by the Odoo editor, there might be rounding errors\n        // during the conversion from RGB to HSL as the HSL system\n        // allows to represent more colors that the RGB hexadecimal\n        // notation (also: hue 360 = hue 0 and should not be averaged to 180).\n        // This also better support random gray palettes.\n        this.grayParams[GRAY_PARAMS.HUE] = !hues.length\n            ? 0\n            : Math.round(\n                  (Math.atan2(\n                      hues\n                          .map((hue) => Math.sin((hue * Math.PI) / 180))\n                          .reduce((memo, value) => memo + value, 0) / hues.length,\n                      hues\n                          .map((hue) => Math.cos((hue * Math.PI) / 180))\n                          .reduce((memo, value) => memo + value, 0) / hues.length\n                  ) *\n                      180) /\n                      Math.PI +\n                      360\n              ) % 360;\n\n        // Average of found saturation diffs, or all grays have no\n        // saturation, or all grays are fully saturated.\n        this.grayParams[GRAY_PARAMS.EXTRA_SATURATION] = saturationDiffs.length\n            ? saturationDiffs.reduce((memo, value) => memo + value, 0) / saturationDiffs.length\n            : oneHasNoSaturation\n            ? -100\n            : 100;\n    }\n    getGrayParams() {\n        return this.grayParams;\n    }\n    getGrays() {\n        return this.grays;\n    }\n    setGrayParams(key, value) {\n        this.grayParams[key] = value;\n    }\n    setGrays(key, value) {\n        this.grays[key] = value;\n    }\n    buildGray(id) {\n        // Getting base grays defined in color_palette.scss\n        const gray = getCSSVariableValue(`base-${id}`, getComputedStyle(document.documentElement));\n        const grayRGB = convertCSSColorToRgba(gray);\n        const hsl = convertRgbToHsl(grayRGB.red, grayRGB.green, grayRGB.blue);\n        const adjustedGrayRGB = convertHslToRgb(\n            this.grayParams[GRAY_PARAMS.HUE],\n            Math.min(\n                Math.max(hsl.saturation + this.grayParams[GRAY_PARAMS.EXTRA_SATURATION], 0),\n                100\n            ),\n            hsl.lightness\n        );\n        return convertRgbaToCSSColor(\n            adjustedGrayRGB.red,\n            adjustedGrayRGB.green,\n            adjustedGrayRGB.blue\n        );\n    }\n\n    getThemeOptionBlock(id, name, options) {\n        // TODO Have a specific kind of options container that takes the specific parameters like name, no element, no selector...\n        const el = this.document.createElement(\"div\");\n        el.dataset.name = name;\n        this.document.body.appendChild(el); // Currently editingElement needs to be isConnected\n\n        options.selector = \"*\";\n\n        return {\n            id: id,\n            element: el,\n            hasOverlayOptions: false,\n            headerMiddleButton: false,\n            isClonable: false,\n            isRemovable: false,\n            options: [options],\n            optionsContainerTopButtons: [],\n            snippetModel: {},\n        };\n    }\n}\n\nexport class CustomizeGrayAction extends BuilderAction {\n    static id = \"customizeGray\";\n    static dependencies = [\"customizeWebsite\", \"themeTab\"];\n    setup() {\n        this.preview = false;\n        this.dependencies.customizeWebsite.withCustomHistory(this);\n    }\n    getValue({ params: { mainParam: grayParamName } }) {\n        return this.dependencies.themeTab.getGrayParams()[grayParamName];\n    }\n    async apply({ params: { mainParam: grayParamName }, value }) {\n        // Gray parameters are used *on the JS side* to compute the grays that\n        // will be saved in the database. We indeed need those grays to be\n        // computed here for faster previews so this allows to not duplicate\n        // most of the logic. Also, this gives flexibility to maybe allow full\n        // customization of grays in custo and themes. Also, this allows to ease\n        // migration if the computation here was to change: the user grays would\n        // still be unchanged as saved in the database.\n\n        this.dependencies.themeTab.setGrayParams(grayParamName, parseInt(value));\n        for (let i = 1; i < 10; i++) {\n            const key = (100 * i).toString();\n            this.dependencies.themeTab.setGrays(key, this.dependencies.themeTab.buildGray(key));\n        }\n\n        // Save all computed (JS side) grays in database\n        await this.dependencies.customizeWebsite.customizeWebsiteColors(\n            this.dependencies.themeTab.getGrays(),\n            {\n                colorType: \"gray\",\n            }\n        );\n    }\n}\nexport class ChangeColorPaletteAction extends CustomizeWebsiteVariableAction {\n    static id = \"changeColorPalette\";\n    static dependencies = [\"customizeWebsite\"];\n    setup() {\n        this.preview = false;\n        this.dependencies.customizeWebsite.withCustomHistory(this);\n    }\n    async load() {\n        const style = this.window.getComputedStyle(this.document.body);\n        const hasCustomizedColors = getCSSVariableValue(\"has-customized-colors\", style);\n        if (hasCustomizedColors && hasCustomizedColors !== \"false\") {\n            return new Promise((resolve) => {\n                this.services.dialog.add(ConfirmationDialog, {\n                    body: _t(\n                        \"Changing the color palette will reset all your color customizations, are you sure you want to proceed?\"\n                    ),\n                    confirm: () => resolve(true),\n                    cancel: () => resolve(false),\n                });\n            });\n        }\n        return true;\n    }\n    async apply(context) {\n        if (!context.loadResult) {\n            return;\n        }\n        await super.apply(context);\n        setBuilderCSSVariables(getHtmlStyle(this.document));\n    }\n}\n\nexport class EditCustomCodeAction extends BuilderAction {\n    static id = \"editCustomCode\";\n    apply() {\n        this.services.dialog.add(EditHeadBodyDialog);\n    }\n}\n\nexport class ConfigureApiKeyAction extends BuilderAction {\n    static id = \"configureApiKey\";\n    static dependencies = [\"googleMapsOption\"];\n    apply() {\n        this.dependencies.googleMapsOption.configureGMapsAPI(\"\", true);\n    }\n}\n\nexport class CustomizePageLayout extends CustomizeWebsiteVariableAction {\n    static id = \"customizePageLayout\";\n\n    async apply(...args) {\n        await super.apply(...args);\n        // since we do not reload on website variables customization we need to\n        // trigger resize to have navbar layout recomputed when we modify page layout\n        this.window.dispatchEvent(new Event(\"resize\"));\n    }\n}\n\nregistry.category(\"website-plugins\").add(ThemeTabPlugin.id, ThemeTabPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { BEGIN, SNIPPET_SPECIFIC, SNIPPET_SPECIFIC_END } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\n\nexport class TimelineImagesOption extends BaseOptionComponent {\n    static template = \"website.TimelineImagesOption\";\n    static selector = \".s_timeline_images\";\n}\n\nexport class DotLinesColorOption extends BaseOptionComponent {\n    static template = \"website.DotLinesColorOption\";\n    static selector = \".s_timeline_images\";\n}\n\nexport class DotColorOption extends BaseOptionComponent {\n    static template = \"website.DotColorOption\";\n    static selector = \".s_timeline_images .s_timeline_images_row\";\n}\n\nclass TimelineImagesOptionPlugin extends Plugin {\n    static id = \"timelineImagesOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [\n            withSequence(BEGIN, TimelineImagesOption),\n            withSequence(SNIPPET_SPECIFIC_END, DotLinesColorOption),\n            withSequence(SNIPPET_SPECIFIC, DotColorOption),\n        ],\n        dropzone_selector: {\n            selector: \".s_timeline_images_row\",\n            dropNear: \".s_timeline_images_row\",\n        },\n        is_movable_selector: { selector: \".s_timeline_images_row\", direction: \"vertical\" },\n    };\n}\n\nregistry.category(\"website-plugins\").add(TimelineImagesOptionPlugin.id, TimelineImagesOptionPlugin);\n", "import { InputConfirmationDialog } from \"@html_builder/snippets/input_confirmation_dialog\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class TranslateAnnouncementScrollPlugin extends Plugin {\n    static id = \"translateAnnouncementScroll\";\n    static dependencies = [\"history\"];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        mark_translatable_nodes: this.listenToAnnouncementScrollClick.bind(this),\n    };\n\n    /**\n     * On click, opens a dialog to translate the interaction's text.\n     */\n    listenToAnnouncementScrollClick() {\n        const announcementScrollEls = this.document.querySelectorAll(\".s_announcement_scroll\");\n\n        for (const announcementScrollEl of announcementScrollEls) {\n            this.addDomListener(announcementScrollEl, \"click\", () => {\n                this.rollbackHistory = this.dependencies.history.makeSavePoint();\n\n                const translatableEl = announcementScrollEl.querySelector(\n                    \".s_announcement_scroll_marquee_item:first-child > [data-oe-translation-source-sha]\"\n                );\n\n                this.services.dialog.add(InputConfirmationDialog, {\n                    defaultValue: translatableEl.textContent,\n                    title: _t(\"Translate Text\"),\n                    confirmLabel: _t(\"Save\"),\n                    cancelLabel: _t(\"Apply\"),\n                    inputLabel: _t(\"Translation\"),\n                    confirm: this.updateText.bind(this, translatableEl),\n                    // Override \"cancel\" to have an \"apply\" button.\n                    cancel: (inputValue) => {\n                        this.updateText.apply(this, [translatableEl, inputValue]);\n                        return false;\n                    },\n                    dismiss: this.rollbackHistory,\n                });\n            });\n        }\n    }\n    /**\n     * Update the translated text.\n     *\n     * @param {HTMLElement} translatableEl\n     * @param {String} inputValue\n     */\n    updateText(translatableEl, inputValue) {\n        if (inputValue !== translatableEl.textContent) {\n            translatableEl.textContent = inputValue;\n            translatableEl.dataset.oeTranslationState = \"translated\";\n            this.dependencies.history.addStep();\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\n\nexport class TranslateLinkInlinePlugin extends Plugin {\n    static id = \"translateLinkInline\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        create_link_handlers: (linkEl) => linkEl.classList.add(\"o_translate_inline\"),\n    };\n}\n\nregistry.category(\"website-plugins\").add(TranslateLinkInlinePlugin.id, TranslateLinkInlinePlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\n\nexport class TranslateSetupEditorPlugin extends Plugin {\n    // TODO: remove in master\n    static id = \"translate_setup_editor_plugin\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        o_editable_selectors: \"[data-oe-model]\",\n    };\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { AttributeTranslateDialog } from \"../translation_components/attributeTranslateDialog\";\nimport { SelectTranslateDialog } from \"../translation_components/selectTranslateDialog\";\nimport {\n    localStorageNoDialogKey,\n    TranslatorInfoDialog,\n} from \"../translation_components/translatorInfoDialog\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\n/**\n * @typedef {((editableEls: HTMLElement[]) => void)[]} mark_translatable_nodes\n */\n\nexport const translationAttributeSelector =\n    '[placeholder*=\"data-oe-translation-source-sha=\"], ' +\n    '[title*=\"data-oe-translation-source-sha=\"], ' +\n    '[value*=\"data-oe-translation-source-sha=\"], ' +\n    '[alt*=\"data-oe-translation-source-sha=\"]';\n\nexport function getTranslationAttributeEls(rootEl) {\n    const translationSavableEls = rootEl.querySelectorAll(translationAttributeSelector);\n    const textAreaEls = Array.from(rootEl.querySelectorAll(\"textarea\")).find((el) =>\n        el.textContent.includes(\"data-oe-translation-source-sha\")\n    );\n    return Array.from(translationSavableEls).concat(textAreaEls || []);\n}\n\n/**\n *\n * @param {HTMLElement} containerEl\n * @returns {HTMLElement[]}\n */\nfunction findOEditable(containerEl) {\n    const isOEditable = (node) => {\n        // Ideally, we should entirely rely on the contenteditable mechanism.\n        // The problem is that the translatable attributes are not branded DOM\n        // nodes hence the o_editable_attribute hack.\n        if (\n            node.isContentEditable ||\n            (node.classList.contains(\"o_editable_attribute\") &&\n                (!node.closest(\".o_not_editable\") || node.classList.contains(\"o_editable_media\")))\n        ) {\n            return true;\n        }\n        return false;\n    };\n    const allDescendantEls = containerEl.querySelectorAll(\"*\");\n    return Array.from(allDescendantEls).filter(isOEditable);\n}\n\nexport class TranslationPlugin extends Plugin {\n    static id = \"translation\";\n    static dependencies = [\"history\"];\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        get_dirty_els: this.getDirtyTranslations.bind(this),\n        after_setup_editor_handlers: () => {\n            const translationSavableEls = getTranslationAttributeEls(\n                this.services.website.pageDocument\n            );\n            for (const translationSavableEl of translationSavableEls) {\n                translationSavableEl.classList.add(\"o_editable_attribute\");\n            }\n            // Apply data-oe-readonly on wrapping editor\n            const editableElSelector = \".o_editable, .o_editable_attribute\";\n            const editableEls = [\n                ...translationSavableEls,\n                ...this.services.website.pageDocument.querySelectorAll(\".o_editable\"),\n            ];\n            for (const editableEl of editableEls) {\n                if (editableEl.querySelectorAll(editableElSelector).length) {\n                    editableEl.setAttribute(\"data-oe-readonly\", \"true\");\n                    editableEl.classList.remove(\"o_editable\", \"o_editable_attribute\");\n                }\n            }\n            return true;\n        },\n        start_edition_handlers: withSequence(5, () => {\n            this.prepareTranslation();\n        }),\n        system_classes: [\"o_editable_attribute\"],\n    };\n\n    setup() {\n        this.websiteService = this.services.website;\n        this.notificationService = this.services.notification;\n        this.dialogService = this.services.dialog;\n    }\n\n    prepareTranslation() {\n        this.editableEls = findOEditable(this.editable);\n        this.buildTranslationInfoMap(this.editableEls);\n        this.handleSelectTranslation(this.editableEls);\n        this.markTranslatableNodes();\n        for (const [translatedEl] of this.elToTranslationInfoMap) {\n            if (translatedEl.matches(\"input[type=hidden].o_translatable_input_hidden\")) {\n                translatedEl.setAttribute(\"type\", \"text\");\n            }\n        }\n\n        // We don't want the BS dropdown to close when clicking in a element to\n        // translate.\n        const menuEls = this.websiteService.pageDocument.querySelectorAll(\".dropdown-menu\");\n        for (const menuEl of menuEls) {\n            this.addDomListener(menuEl, \"click\", (ev) => {\n                const editableEl = ev.target.closest(\".o_editable\");\n                if (editableEl && menuEl.contains(editableEl)) {\n                    ev.stopPropagation();\n                    ev.preventDefault();\n                }\n            });\n        }\n\n        if (!browser.localStorage.getItem(localStorageNoDialogKey)) {\n            this.dialogService.add(TranslatorInfoDialog);\n        }\n\n        const showNotification = (ev) => {\n            // Prevent duplicate notifications for the same click but allow the\n            // event to bubble (i.e. for carousel sliding)\n            if (ev.__shownNotification) {\n                return;\n            }\n            ev.__shownNotification = true;\n            let message = _t(\"This translation is not editable.\");\n            if (ev.target.closest(\".s_table_of_content_navbar_wrap\")) {\n                message = _t(\"Translate header in the text. Menu is generated automatically.\");\n            }\n            this.notificationService.add(message, {\n                type: \"info\",\n                sticky: false,\n            });\n        };\n        for (const translateEl of this.editableEls) {\n            this.handleToC(translateEl);\n        }\n        const savableInsideNotEditableEls = this.editable.querySelectorAll(\n            \".o_not_editable .o_editable, .o_not_editable .o_editable_attribute\"\n        );\n        for (const savableInsideNotEditableEl of savableInsideNotEditableEls) {\n            this.addDomListener(savableInsideNotEditableEl, \"click\", showNotification);\n        }\n        // Keep the original values of elToTranslationInfoMap so that we know\n        // which translations have been updated.\n        this.originalElToTranslationInfoMap = new Map();\n        for (const [translateEl, translationInfo] of this.elToTranslationInfoMap) {\n            this.originalElToTranslationInfoMap.set(\n                translateEl,\n                JSON.parse(JSON.stringify(translationInfo))\n            );\n        }\n    }\n\n    /**\n     * Creates a map that links html elements to their attributes to translate.\n     * It has the form:\n     * {translateEl1: {\n     *     attribute1: {\n     *         oeModel: \"ir.ui.view\",\n     *         oeId: \"5\",\n     *         oeField: \"arch_db\",\n     *         oeTranslationState: \"translated\",\n     *         oeTranslationSourceSha: \"123\",\n     *         translation: \"traduction\",\n     *     },\n     * }};\n     *\n     * @param {HTMLElement[]} editableEls\n     */\n    buildTranslationInfoMap(editableEls) {\n        this.elToTranslationInfoMap = new Map();\n        const translatedAttrs = [\"placeholder\", \"title\", \"alt\", \"value\"];\n        const translationRegex =\n            /<span [^>]*data-oe-translation-source-sha=\"([^\"]+)\"[^>]*>(.*)<\\/span>/;\n        const isEmpty = (el) => !el.hasChildNodes() || el.innerHTML.trim() === \"\";\n        const matchTag = (el) => el.matches(\"input, select, textarea, img\");\n        for (const translatedAttr of translatedAttrs) {\n            const filteredEditableEls = editableEls.filter(\n                (editableEl) =>\n                    editableEl.hasAttribute(translatedAttr) &&\n                    editableEl\n                        .getAttribute(translatedAttr)\n                        .includes(\"data-oe-translation-source-sha=\") &&\n                    (isEmpty(editableEl) || matchTag(editableEl))\n            );\n            for (const filteredEditableEl of filteredEditableEls) {\n                const translation = filteredEditableEl.getAttribute(translatedAttr);\n                this.updateTranslationMap(filteredEditableEl, translation, translatedAttr);\n                const match = translation.match(translationRegex);\n                filteredEditableEl.setAttribute(translatedAttr, match[2]);\n                if (translatedAttr === \"value\") {\n                    filteredEditableEl.value = match[2];\n                }\n                filteredEditableEl.classList.add(\"o_translatable_attribute\");\n            }\n        }\n        const textEditEls = editableEls.filter(\n            (editableEl) =>\n                editableEl.matches(\"textarea\") &&\n                editableEl.textContent.includes(\"data-oe-translation-source-sha\")\n        );\n        for (const textEditEl of textEditEls) {\n            const translation = textEditEl.textContent;\n            this.updateTranslationMap(textEditEl, translation, \"textContent\");\n            const match = translation.match(translationRegex);\n            textEditEl.value = match[2];\n            // Update the text content of textarea too\n            textEditEl.innerText = match[2];\n            textEditEl.classList.add(\"o_translatable_text\");\n            textEditEl.classList.remove(\"o_text_content_invisible\");\n        }\n    }\n\n    handleSelectTranslation(editableEls) {\n        // Hack: we add a temporary element to handle option's text translations\n        // from the linked <select/>. The final values are copied to the\n        // original element right before save.\n        const selectEls = editableEls.filter((editableEl) =>\n            editableEl.matches(\"[data-oe-translation-source-sha] > select\")\n        );\n        this.translateSelectEls = [];\n        for (const selectEl of selectEls) {\n            const selectTranslationEl = document.createElement(\"div\");\n            selectTranslationEl.className = \"o_translation_select\";\n            const optionNames = [...selectEl.options].map((option) => option.text);\n            for (const optionName of optionNames) {\n                const optionEl = document.createElement(\"div\");\n                optionEl.textContent = optionName;\n                optionEl.dataset.initialTranslationValue = optionName;\n                optionEl.className = \"o_translation_select_option\";\n                selectTranslationEl.appendChild(optionEl);\n                this.translateSelectEls.push(optionEl);\n            }\n            selectEl.before(selectTranslationEl);\n        }\n    }\n\n    handleToC(translateEl) {\n        if (translateEl.closest(\".s_table_of_content_navbar_wrap\")) {\n            // Make sure the same translation ids are used\n            const href = translateEl.closest(\"a\").getAttribute(\"href\");\n            const headerEl = translateEl\n                .closest(\".s_table_of_content\")\n                .querySelector(`${href} [data-oe-translation-source-sha]`);\n            if (headerEl) {\n                if (\n                    translateEl.dataset.oeTranslationSourceSha !==\n                    headerEl.dataset.oeTranslationSourceSha\n                ) {\n                    // Use the same identifier for the generated navigation\n                    // label and its associated header so that the general\n                    // synchronization mechanism kicks in.\n                    // The initial value is kept to be restored before save in\n                    // order to keep the translation of the unstyled label\n                    // distinct from the one of the header.\n                    translateEl.dataset.oeTranslationSaveSha =\n                        translateEl.dataset.oeTranslationSourceSha;\n                    translateEl.dataset.oeTranslationSourceSha =\n                        headerEl.dataset.oeTranslationSourceSha;\n                }\n                // TODO: handle o_translation_without_style\n                translateEl.classList.add(\"o_translation_without_style\");\n            }\n        }\n    }\n\n    markTranslatableNodes() {\n        // attributes\n        for (const [translateEl, translationInfo] of this.elToTranslationInfoMap) {\n            for (const translationData of Object.values(translationInfo)) {\n                // If a node has an already translated attribute, we don't need\n                // to update its state, since it can be set again as\n                // \"to_translate\" by other attributes...\n                if (translateEl.dataset.oeTranslationState !== \"translated\") {\n                    translateEl.setAttribute(\n                        \"data-oe-translation-state\",\n                        translationData.oeTranslationState || \"to_translate\"\n                    );\n                }\n            }\n            this.addDomListener(translateEl, \"click\", (ev) => {\n                const translateEl = ev.target;\n                const elToTranslationInfoMap = this.elToTranslationInfoMap;\n                this.dialogService.add(AttributeTranslateDialog, {\n                    node: translateEl,\n                    elToTranslationInfoMap: elToTranslationInfoMap,\n                    addStep: this.dependencies.history.addStep,\n                    applyCustomMutation: this.dependencies.history.applyCustomMutation,\n                });\n            });\n        }\n        for (const translateSelectEl of this.translateSelectEls) {\n            this.addDomListener(translateSelectEl, \"click\", (ev) => {\n                const translateSelectEl = ev.target;\n                this.dialogService.add(SelectTranslateDialog, {\n                    node: translateSelectEl,\n                    addStep: this.dependencies.history.addStep,\n                });\n            });\n        }\n        this.dispatchTo(\"mark_translatable_nodes\", this.editableEls);\n    }\n\n    updateTranslationMap(translateEl, translation, attrName) {\n        const parser = new DOMParser();\n        const dummyDoc = parser.parseFromString(translation, \"text/html\");\n        const translationEl = dummyDoc.querySelector(\"[data-oe-translation-source-sha]\");\n        if (!this.elToTranslationInfoMap.get(translateEl)) {\n            this.elToTranslationInfoMap.set(translateEl, {});\n        }\n        this.elToTranslationInfoMap.get(translateEl)[attrName] = translationEl.dataset;\n        this.elToTranslationInfoMap.get(translateEl)[attrName].translation =\n            translationEl.innerHTML;\n    }\n\n    /**\n     * Gets the modified translations\n     * @returns {HTMLElement[]}\n     */\n    getDirtyTranslations() {\n        const dirtyEls = [];\n        for (const [translateEl, translationInfo] of this.elToTranslationInfoMap) {\n            for (const [attr, data] of Object.entries(translationInfo)) {\n                if (\n                    this.originalElToTranslationInfoMap.get(translateEl)[attr].translation !==\n                    data.translation\n                ) {\n                    const spanEl = document.createElement(\"span\");\n                    for (const [name, value] of Object.entries(data)) {\n                        spanEl.dataset[name] = value;\n                    }\n                    const translation = spanEl.dataset.translation;\n                    delete spanEl.dataset.translation;\n                    spanEl.innerHTML = translation;\n                    dirtyEls.push(spanEl);\n                }\n            }\n        }\n        return dirtyEls;\n    }\n\n    cleanForSave({ root }) {\n        root.querySelectorAll(\".o_editable_attribute\").forEach((el) => {\n            el.classList.remove(\"o_editable_attribute\");\n        });\n        // Remove the `.o_translation_select` temporary element\n        const optionsEl = root.querySelector(\".o_translation_select\");\n        if (optionsEl) {\n            const selectEl = optionsEl.nextElementSibling;\n            const translatedOptions = optionsEl.children;\n            const selectOptions = selectEl.tagName === \"SELECT\" ? [...selectEl.options] : [];\n            if (selectOptions.length === translatedOptions.length) {\n                selectOptions.map((option, i) => {\n                    option.text = translatedOptions[i].textContent;\n                });\n            }\n            optionsEl.remove();\n        }\n        if (root.dataset.oeTranslationSaveSha) {\n            root.dataset.oeTranslationSourceSha = root.dataset.oeTranslationSaveSha;\n            delete root.dataset.oeTranslationSaveSha;\n        }\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { OptionsContainer } from \"@html_builder/sidebar/option_container\";\n\nexport class CustomizeTranslationTab extends Component {\n    static template = \"website.CustomizeTranslationTab\";\n    static components = { OptionsContainer };\n    static props = {};\n    setup() {\n        this.optionsContainers = this.env.editor.resources[\"translate_options\"];\n    }\n}\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { reactive } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { TranslateWebpageOption } from \"./translate_webpage_option\";\n\n/**\n * @typedef { Object } CustomizeTranslationTabShared\n * @property { CustomizeTranslationTabPlugin['getTranslationState'] } getTranslationState\n */\n\n/**\n * Action to translate the entire webpage using AI.\n */\nclass TranslateToAction extends BuilderAction {\n    static id = \"translateWebpageAI\";\n    static dependencies = [\"customizeTranslationTab\"];\n\n    async apply() {\n        const translationState = this.dependencies.customizeTranslationTab.getTranslationState();\n        try {\n            translationState.isTranslating = true;\n            const language = this.services.website.currentWebsite.metadata.langName;\n            const { translationChunks, translationMap } = this.generateTranslationChunks(\n                this.editable\n            );\n            if (translationChunks) {\n                const responses = await this.runTranslationChunks(translationChunks, language);\n                const failedNodeCount = this.applyTranslationsToDOM(translationMap, responses);\n                if (failedNodeCount > 0) {\n                    this.showNotification(\n                        _t(\n                            \"%s text blocks were skipped during translation. Please try again.\",\n                            failedNodeCount\n                        ),\n                        _t(\"Translation Error\"),\n                        \"danger\"\n                    );\n                }\n            }\n        } finally {\n            translationState.isTranslating = false;\n        }\n    }\n\n    /**\n     * Determines if a text node should be skipped for translation.\n     * Skip if it contains no letters/numbers or is likely an email, phone\n     * number or URL.\n     *\n     * @param {Node} el - Text node to evaluate\n     * @return {boolean} True if the node should be skipped\n     */\n    shouldSkipTranslation(el) {\n        const text = el.textContent.replace(/[\\u200B-\\u200D\\uFEFF]/g, \"\").trim();\n        const EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n        const PHONE_REGEX = /^[+\\d][\\d\\s\\-().]{6,}$/;\n        const URL_REGEX = /^(https?:\\/\\/)?([\\w-]+\\.)+[\\w-]+(:\\d+)?(\\/[\\w\\-./?%&=]*)?(#\\S*)?$/i;\n        const LETTER_OR_NUMBER_REGEX = /\\p{L}|\\p{N}/u;\n        return (\n            !LETTER_OR_NUMBER_REGEX.test(text) ||\n            EMAIL_REGEX.test(text) ||\n            PHONE_REGEX.test(text) ||\n            URL_REGEX.test(text)\n        );\n    }\n\n    /**\n     * Collects translatable text nodes in the DOM and group them into chunks.\n     * Each chunk is limited in size to avoid overloading the translation API.\n     *\n     * @param {HTMLElement} containerEl - Root element\n     * @param {number} limit - Max characters per chunk\n     * @return {Object} { List of chunks, Map of original nodes by their IDs }\n     */\n    generateTranslationChunks(containerEl, limit = 2000) {\n        const elements = Array.from(\n            containerEl.querySelectorAll(\"[data-oe-translation-state='to_translate']\")\n        ).filter(\n            (el) =>\n                // TODO: fix `o_frontend_to_backend_buttons` to have no\n                // attribute `data-oe-translation-state`\n                !el.closest(\".o_not_editable, .o_frontend_to_backend_buttons\") &&\n                // Skip attribute translations, will handle in task-5047714\n                !el.classList.contains(\"o_translatable_attribute\")\n        );\n\n        const translationChunks = [];\n        const translationMap = new Map();\n        let currentChunk = [];\n        let currentChunkLength = 0;\n        const flushChunk = () => {\n            if (currentChunk.length) {\n                translationChunks.push(currentChunk);\n                currentChunk = [];\n                currentChunkLength = 0;\n            }\n        };\n\n        for (const el of elements) {\n            const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);\n            while (walker.nextNode()) {\n                const nodeId = uniqueId(\"t_\");\n                const node = walker.currentNode;\n                if (this.shouldSkipTranslation(node)) {\n                    continue;\n                }\n                const text = node.textContent.trim();\n                const itemSize = JSON.stringify({ id: nodeId, text }).length;\n                if (currentChunkLength + itemSize > limit && currentChunk.length) {\n                    flushChunk();\n                }\n                currentChunk.push({ el: node, id: nodeId, originalText: text });\n                translationMap.set(nodeId, node);\n                currentChunkLength += itemSize;\n            }\n        }\n        // If any chunk left, flush it\n        flushChunk();\n\n        if (!translationMap.size) {\n            this.showNotification(\n                _t(\"No translatable content found in the current webpage.\"),\n                _t(\"Translation Info\"),\n                \"info\"\n            );\n            return {};\n        }\n        return { translationChunks, translationMap };\n    }\n\n    /**\n     * Translates each chunk with limited concurrency.\n     *\n     * @param {Array} translationChunks - List of chunks to translate\n     * @param {string} language - Target language code\n     * @return {Promise<Array>} Server responses for each chunk\n     */\n    async runTranslationChunks(translationChunks, language) {\n        const systemMessage = {\n            role: \"system\",\n            content:\n                \"You are a translation assistant. Your goal is to translate multiple text blocks.\\n\" +\n                \"Instructions:\\n\" +\n                \"- Input will be an array of objects: [{id: string, text: string}, ...]\\n\" +\n                \"- Return ONLY valid JSON in the same array format, replacing 'text' with the translated text.\\n\" +\n                \"- Do not add comments or extra fields.\",\n        };\n\n        const tasks = translationChunks.map((chunk) => async () => {\n            const prompt = JSON.stringify(\n                chunk.map(({ id, originalText }) => ({ id, text: originalText }))\n            );\n            const conversation = [\n                systemMessage,\n                { role: \"user\", content: `Translate the following to ${language}:\\n\\n${prompt}` },\n            ];\n            return rpc(\n                \"/html_editor/generate_text\",\n                {\n                    prompt: prompt,\n                    conversation_history: conversation,\n                },\n                { silent: true }\n            );\n        });\n\n        // Limit concurrency to avoid\n        // \"Oops, it looks like our AI is unreachable!\" error\n        // when too many requests are sent in a short time.\n        const concurrencyLimit = 5;\n        const allResults = [];\n        const executing = new Set();\n        for (const task of tasks) {\n            if (executing.size >= concurrencyLimit) {\n                await Promise.race(executing);\n            }\n            const promise = task().finally(() => executing.delete(promise));\n            executing.add(promise);\n            allResults.push(promise);\n        }\n        return Promise.all(allResults);\n    }\n\n    /**\n     * Parses translation responses and update DOM nodes in-place.\n     * Returns the failed translation nodes count.\n     *\n     * @param {Map<string, Object} translationMap - Original Nodes mapped by their IDs\n     * @param {Array} responses - Translated text responses\n     * @return {Number} Count of failed translation nodes\n     */\n    applyTranslationsToDOM(translationMap, responses) {\n        let numOfFailedTranslationNodes = 0;\n        for (const response of responses) {\n            let translations;\n            try {\n                translations = JSON.parse(response);\n            } catch {\n                numOfFailedTranslationNodes += translationMap.size;\n                continue;\n            }\n\n            for (const { id, text } of translations) {\n                const node = translationMap.get(id);\n                if (!node) {\n                    continue;\n                }\n                const translated = (text || \"\").trim();\n                if (!translated) {\n                    numOfFailedTranslationNodes++;\n                    continue;\n                }\n                node.textContent = translated;\n                const parentEl = node.parentElement?.closest(\"[data-oe-translation-state]\");\n                if (parentEl) {\n                    parentEl.dataset.oeTranslationState = \"translated\";\n                }\n            }\n        }\n        return numOfFailedTranslationNodes;\n    }\n\n    showNotification(message, title, type) {\n        this.services.notification.add(message, {\n            title: title,\n            type,\n            sticky: true,\n        });\n    }\n}\n\n/**\n * Plugin that adds a \"Translation\" tab to the sidebar and provides AI-powered\n * options to translate the entire webpage.\n */\nexport class CustomizeTranslationTabPlugin extends Plugin {\n    static id = \"customizeTranslationTab\";\n    static shared = [\"getTranslationState\"];\n\n    translationState = reactive({\n        isTranslating: false,\n    });\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_actions: {\n            TranslateToAction,\n        },\n        translate_options: [\n            withSequence(\n                1,\n                this.getTranslationOptionBlock(\n                    \"translate-webpage\",\n                    _t(\"Translation\"),\n                    TranslateWebpageOption\n                )\n            ),\n        ],\n    };\n\n    getTranslationState() {\n        return this.translationState;\n    }\n\n    /**\n     * Prepares and returns a translation option block for the sidebar.\n     *\n     * @param {string} id - Unique identifier for the block\n     * @param {string} name - Display name for the block\n     * @param {Object} Option - Option component\n     */\n    getTranslationOptionBlock(id, name, Option) {\n        const el = this.document.createElement(\"div\");\n        el.dataset.name = name;\n        this.document.body.appendChild(el);\n\n        return {\n            id: id,\n            snippetModel: {},\n            element: el,\n            options: [Option],\n            isRemovable: false,\n            isClonable: false,\n            containerTopButtons: [],\n        };\n    }\n}\n\nregistry\n    .category(\"translation-plugins\")\n    .add(CustomizeTranslationTabPlugin.id, CustomizeTranslationTabPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { useState } from \"@odoo/owl\";\n\nexport class TranslateWebpageOption extends BaseOptionComponent {\n    static template = \"website.TranslateWebpageOption\";\n    static selector = \"*\";\n    setup() {\n        super.setup();\n        this.translationState = useState(\n            this.env.editor.shared.customizeTranslationTab.getTranslationState()\n        );\n    }\n}\n", "", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { BOX_BORDER_SHADOW } from \"../option_sequence\";\nimport { registry } from \"@web/core/registry\";\nimport { BaseVerticalAlignmentOption } from \"@html_builder/plugins/base_vertical_alignment_option\";\n\nexport class WebsiteVerticalAlignmentOption extends BaseVerticalAlignmentOption {\n    static selector = \".s_attributes_vertical_col\";\n    static applyTo = \":scope > .row\";\n    level = 0;\n    justify = false;\n}\n\nclass VerticalAlignmentOptionPlugin extends Plugin {\n    static id = \"websiteVerticalAlignmentOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [withSequence(BOX_BORDER_SHADOW, WebsiteVerticalAlignmentOption)],\n    };\n}\nregistry\n    .category(\"website-plugins\")\n    .add(VerticalAlignmentOptionPlugin.id, VerticalAlignmentOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\n\n/**\n * @typedef { Object } WebsiteSessionShared\n * @property { WebsiteSessionPlugin['getSession'] } getSession\n */\n\nexport class WebsiteSessionPlugin extends Plugin {\n    static id = \"websiteSession\";\n    static shared = [\"getSession\"];\n\n    getSession() {\n        return this.window.odoo.loader.modules.get(\"@web/session\").session;\n    }\n}\n\nregistry.category(\"website-plugins\").add(WebsiteSessionPlugin.id, WebsiteSessionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { DEVICE_VISIBILITY_OPTION_SELECTOR } from \"./options/visibility_option_plugin\";\nimport { VisibilityOption } from \"./options/visibility_option\";\n\nexport class WebsiteVisibilityPlugin extends Plugin {\n    static id = \"websiteVisibilityPlugin\";\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        system_classes: [\"o_conditional_hidden\"],\n        target_show: this.onTargetShow.bind(this),\n        target_hide: this.onTargetHide.bind(this),\n    };\n\n    onTargetHide(editingEl) {\n        if (\n            editingEl.matches(DEVICE_VISIBILITY_OPTION_SELECTOR) ||\n            editingEl.matches(VisibilityOption.selector)\n        ) {\n            editingEl.classList.remove(\"o_snippet_override_invisible\");\n\n            const isConditionalHidden = editingEl.matches(\"[data-visibility='conditional']\");\n            if (isConditionalHidden) {\n                editingEl.classList.add(\"o_conditional_hidden\");\n            }\n        }\n    }\n\n    onTargetShow(editingEl) {\n        if (\n            editingEl.matches(DEVICE_VISIBILITY_OPTION_SELECTOR) ||\n            editingEl.matches(VisibilityOption.selector)\n        ) {\n            const isMobilePreview = this.config.isMobileView(editingEl);\n            const isMobileHidden = editingEl.classList.contains(\"o_snippet_mobile_invisible\");\n            const isDesktopHidden = editingEl.classList.contains(\"o_snippet_desktop_invisible\");\n            if ((isMobileHidden && isMobilePreview) || (isDesktopHidden && !isMobilePreview)) {\n                editingEl.classList.add(\"o_snippet_override_invisible\");\n            }\n\n            editingEl.classList.remove(\"o_conditional_hidden\");\n        }\n    }\n}\n\nregistry.category(\"website-plugins\").add(WebsiteVisibilityPlugin.id, WebsiteVisibilityPlugin);\n", "import { SnippetModel } from \"@html_builder/snippets/snippet_service\";\nimport { applyTextHighlight } from \"@website/js/highlight_utils\";\nimport { registry } from \"@web/core/registry\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npatch(SnippetModel.prototype, {\n    /**\n     * @override\n     */\n    updateSnippetContent(snippetEl) {\n        super.updateSnippetContent(...arguments);\n        // Build the highlighted text content for new added snippets.\n        for (const textEl of snippetEl?.querySelectorAll(\".o_text_highlight\") || []) {\n            applyTextHighlight(textEl);\n        }\n    },\n\n    /**\n     * @override\n     */\n    getSnippetLabel(snippetEl) {\n        const label = super.getSnippetLabel(snippetEl);\n        // Check if any element in the snippet has the \"parallax\" class to show\n        // the \"Parallax\" label. This must be done this way because a theme or\n        // custom snippet may add or remove parallax elements. Note that if a\n        // label is already set, we do not change it.\n        if (!label) {\n            const contentEl = snippetEl.children[0];\n            if (contentEl.matches(\".parallax\") || !!contentEl.querySelector(\".parallax\")) {\n                return _t(\"Parallax\");\n            }\n        }\n        return label;\n    },\n});\n\nregistry\n    .category(\"html_builder.snippetsPreprocessor\")\n    .add(\"website_snippets\", (namespace, snippets) => {\n        if (namespace === \"website.snippets\") {\n            // This should be empty in master, it is used to fix snippets in stable.\n        }\n    });\n", "import { SnippetViewer } from \"@html_builder/snippets/snippet_viewer\";\nimport { onMounted, onPatched, onWillPatch, onWillUnmount } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(SnippetViewer.prototype, {\n    setup() {\n        super.setup();\n\n        if (this.props.snippetModel.snippetsName === \"website.snippets\") {\n            this.websiteService = useService(\"website\");\n            this.innerWebsiteEditService =\n                this.websiteService.websiteRootInstance?.bindService(\"website_edit\");\n            this.previousSearch = \"\";\n\n            const updatePreview = () => {\n                if (this.innerWebsiteEditService) {\n                    this.innerWebsiteEditService.update(this.content.el, \"preview\");\n                }\n            };\n            const stopPreview = () => {\n                if (this.innerWebsiteEditService) {\n                    this.innerWebsiteEditService.stop(this.content.el);\n                }\n            };\n            onMounted(updatePreview);\n            onPatched(updatePreview);\n\n            onWillPatch(stopPreview);\n            onWillUnmount(stopPreview);\n        }\n    },\n});\n", "import { Component } from \"@odoo/owl\";\nimport { WebsiteDialog } from \"@website/components/dialog/dialog\";\n\nexport class AttributeTranslateDialog extends Component {\n    static components = { WebsiteDialog };\n    static template = \"website_builder.AttributeTranslateDialog\";\n    static props = {\n        node: { validate: (p) => p.nodeType === Node.ELEMENT_NODE },\n        elToTranslationInfoMap: Object,\n        addStep: Function,\n        applyCustomMutation: Function,\n        close: Function,\n    };\n\n    setup() {\n        this.modifiedAttrs = {};\n    }\n\n    onInputChange(ev) {\n        const inputEl = ev.target;\n        const attr = inputEl.previousSibling.textContent;\n        const translateEl = this.props.node;\n        const newValue = inputEl.value;\n        this.modifiedAttrs[attr] = newValue;\n        if (attr !== \"textContent\") {\n            translateEl.setAttribute(attr, newValue);\n            if (attr === \"value\") {\n                translateEl.value = newValue;\n            }\n        } else {\n            translateEl.value = newValue;\n        }\n        translateEl.classList.add(\"oe_translated\");\n    }\n\n    get translationInfos() {\n        return this.props.elToTranslationInfoMap.get(this.props.node);\n    }\n\n    addStepAndClose() {\n        const oldValue = JSON.parse(JSON.stringify(this.translationInfos));\n        this.props.applyCustomMutation({\n            apply: () => {\n                for (const [attr, newValue] of Object.entries(this.modifiedAttrs)) {\n                    this.translationInfos[attr].translation = newValue;\n                }\n            },\n            revert: () => {\n                for (const attr of Object.keys(this.modifiedAttrs)) {\n                    this.translationInfos[attr].translation = oldValue[attr].translation;\n                }\n            },\n        });\n        this.props.addStep();\n        this.props.close();\n    }\n}\n", "import { Component, useRef } from \"@odoo/owl\";\nimport { WebsiteDialog } from \"@website/components/dialog/dialog\";\n\n// Used to translate the text of `<select/>` options since it should not be\n// possible to interact with the content of `.o_translation_select` elements.\nexport class SelectTranslateDialog extends Component {\n    static components = { WebsiteDialog };\n    static template = \"website_builder.SelectTranslateDialog\";\n    static props = {\n        node: { validate: (p) => p.nodeType === Node.ELEMENT_NODE },\n        addStep: Function,\n        close: Function,\n    };\n    setup() {\n        this.inputEl = useRef(\"input\");\n    }\n\n    onInputChange() {\n        const value = this.inputEl.el.value;\n        this.optionEl.textContent = value;\n        this.optionEl.classList.toggle(\n            \"oe_translated\",\n            value !== this.optionEl.dataset.initialTranslationValue\n        );\n    }\n\n    get optionEl() {\n        return this.props.node;\n    }\n\n    addStepAndClose() {\n        this.props.addStep();\n        this.props.close();\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { WebsiteDialog } from \"@website/components/dialog/dialog\";\n\nexport const localStorageNoDialogKey = \"website_translator_nodialog\";\n\nexport class TranslatorInfoDialog extends Component {\n    static components = { WebsiteDialog };\n    static template = \"website_builder.TranslatorInfoDialog\";\n    static props = {\n        close: Function,\n    };\n    setup() {\n        this.strongOkButton = _t(\"Ok, never show me this again\");\n        this.okButton = _t(\"Ok\");\n    }\n\n    onStrongOkClick() {\n        browser.localStorage.setItem(localStorageNoDialogKey, true);\n    }\n}\n", "import { Builder } from \"@html_builder/builder\";\nimport { BuilderOptionsTranslationPlugin } from \"@html_builder/core/builder_options_plugin_translate\";\nimport { CORE_PLUGINS, MAIN_PLUGINS } from \"@html_builder/core/core_plugins\";\nimport { DisableSnippetsPlugin } from \"@html_builder/core/disable_snippets_plugin_translation\";\nimport { OperationPlugin } from \"@html_builder/core/operation_plugin\";\nimport { SavePlugin } from \"@html_builder/core/save_plugin\";\nimport { SetupEditorPlugin } from \"@html_builder/core/setup_editor_plugin\";\nimport { TranslateSetupEditorPlugin } from \"./plugins/translate_setup_editor_plugin\";\nimport { VisibilityPlugin } from \"@html_builder/core/visibility_plugin\";\nimport { removePlugins } from \"@html_builder/utils/utils\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { Component } from \"@odoo/owl\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { HighlightPlugin } from \"./plugins/highlight/highlight_plugin\";\nimport { PopupVisibilityPlugin } from \"./plugins/popup_visibility_plugin\";\nimport { SaveTranslationPlugin } from \"./plugins/save_translation_plugin\";\nimport { TranslateAnnouncementScrollPlugin } from \"./plugins/translate_announcement_scroll_plugin\";\nimport { TranslateLinkInlinePlugin } from \"./plugins/translate_link_inline_plugin\";\nimport { TranslationPlugin } from \"./plugins/translation_plugin\";\nimport { WebsiteVisibilityPlugin } from \"./plugins/website_visibility_plugin\";\nimport { EditInteractionPlugin } from \"./plugins/edit_interaction_plugin\";\nimport { AnimateOptionPlugin } from \"./plugins/options/animate_option_plugin\";\nimport { BuilderComponentPlugin } from \"@html_builder/core/builder_component_plugin\";\nimport { BuilderActionsPlugin } from \"@html_builder/core/builder_actions_plugin\";\nimport { CoreBuilderActionPlugin } from \"@html_builder/core/core_builder_action_plugin\";\nimport { CarouselOptionTranslationPlugin } from \"./plugins/carousel_option_translation_plugin\";\nimport { OverlayButtonsPlugin } from \"@html_builder/core/overlay_buttons/overlay_buttons_plugin\";\nimport { DropZonePlugin } from \"@html_builder/core/drop_zone_plugin\";\nimport { DropZoneSelectorPlugin } from \"@html_builder/core/dropzone_selector_plugin\";\nimport { CustomizeTabPlugin } from \"@html_builder/core/customize_tab_plugin\";\nimport { BuilderOverlayPlugin } from \"@html_builder/core/builder_overlay/builder_overlay_plugin\";\nimport { WebsiteSetupEditorPlugin } from \"./plugins/setup_editor_plugin\";\nimport { ThemeTab } from \"./plugins/theme/theme_tab\";\nimport { TranslateTableOfContentOptionPlugin } from \"./plugins/options/table_of_content_option_plugin_translate\";\nimport { FieldChangeReplicationPlugin } from \"@html_builder/core/field_change_replication_plugin\";\nimport { BuilderContentEditablePlugin } from \"@html_builder/core/builder_content_editable_plugin\";\nimport { ImageFieldPlugin } from \"@html_builder/plugins/image_field_plugin\";\nimport { MonetaryFieldPlugin } from \"@html_builder/plugins/monetary_field_plugin\";\nimport { Many2OneOptionPlugin } from \"@html_builder/plugins/many2one_option_plugin\";\nimport { CustomizeTranslationTab } from \"@website/builder/plugins/translation_tab/customize_translation_tab\";\nimport { CustomizeTranslationTabPlugin } from \"./plugins/translation_tab/customize_translation_tab_plugin\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { revertPreview } from \"@html_builder/core/utils\";\n\nconst TRANSLATION_PLUGINS = [\n    BuilderOptionsTranslationPlugin,\n    BuilderActionsPlugin,\n    BuilderComponentPlugin,\n    CoreBuilderActionPlugin,\n    DisableSnippetsPlugin,\n    SavePlugin,\n    SetupEditorPlugin,\n    TranslateSetupEditorPlugin,\n    WebsiteSetupEditorPlugin,\n    VisibilityPlugin,\n    PopupVisibilityPlugin,\n    SaveTranslationPlugin,\n    TranslateLinkInlinePlugin,\n    TranslationPlugin,\n    TranslateAnnouncementScrollPlugin,\n    WebsiteVisibilityPlugin,\n    AnimateOptionPlugin,\n    HighlightPlugin,\n    OperationPlugin,\n    EditInteractionPlugin,\n    TranslateTableOfContentOptionPlugin,\n    CarouselOptionTranslationPlugin,\n    FieldChangeReplicationPlugin,\n    BuilderContentEditablePlugin,\n    ImageFieldPlugin,\n    MonetaryFieldPlugin,\n    Many2OneOptionPlugin,\n    CustomizeTranslationTabPlugin,\n    // Those plugin are depended by other Plugin but not used in translation\n    // mode.\n    // Todo: find a better way to handle that.\n    class FakeRemovePlugin extends Plugin {\n        static id = \"remove\";\n    },\n    class FakeClonePlugin extends Plugin {\n        static id = \"clone\";\n    },\n];\n\nexport class WebsiteBuilder extends Component {\n    static template = \"website.WebsiteBuilder\";\n    static components = { Builder };\n    static props = {\n        translation: { type: Boolean },\n        builderProps: { type: Object },\n    };\n\n    setup() {\n        this.websiteService = useService(\"website\");\n        this.dialog = useService(\"dialog\");\n        useSetupAction({\n            beforeUnload: (ev) => this.onBeforeUnload(ev),\n            beforeLeave: () => this.onBeforeLeave(),\n        });\n    }\n\n    async discard() {\n        await revertPreview(this.editor);\n        if (this.editor.shared.history.canUndo()) {\n            this.dialog.add(ConfirmationDialog, {\n                title: _t(\"Discard all changes?\"),\n                body: _t(\n                    \"Are you sure you want to discard all your changes? Once you do, they're gone for good.\"\n                ),\n                confirmLabel: _t(\"Discard changes\"),\n                cancelLabel: _t(\"Keep editing\"),\n                confirm: () => this.props.builderProps.closeEditor(),\n                cancel: () => {},\n            });\n        } else {\n            this.props.builderProps.closeEditor();\n        }\n    }\n\n    onBeforeUnload(event) {\n        if (!this.editor) {\n            return;\n        }\n        if (this.editor.shared.history.canUndo()) {\n            event.preventDefault();\n            event.returnValue = \"Unsaved changes\";\n        }\n    }\n\n    async onBeforeLeave() {\n        if (!this.editor) {\n            return true;\n        }\n        if (this.editor.shared.history.canUndo()) {\n            let continueProcess = true;\n            await new Promise((resolve) => {\n                this.dialog.add(ConfirmationDialog, {\n                    body: _t(\"If you proceed, your changes will be lost\"),\n                    confirmLabel: _t(\"Continue\"),\n                    confirm: () => resolve(),\n                    cancel: () => {\n                        continueProcess = false;\n                        resolve();\n                    },\n                });\n            });\n            return continueProcess;\n        }\n        return true;\n    }\n\n    async save() {\n        // TODO: handle the urgent save and the fail of the save operation\n        await this.editor.shared.operation.next(\n            async () => {\n                await this.editor.shared.savePlugin.save();\n                this.props.builderProps.closeEditor();\n            },\n            { withLoadingEffect: false }\n        );\n    }\n\n    get builderProps() {\n        const builderProps = Object.assign({}, this.props.builderProps);\n        const websitePlugins = this.props.translation\n            ? [...TRANSLATION_PLUGINS, ...registry.category(\"website-translation-plugins\").getAll()]\n            : [\n                  ...registry.category(\"builder-plugins\").getAll(),\n                  ...registry.category(\"website-plugins\").getAll(),\n              ];\n        const builderPluginsToRemove = [\n            // Currently empty.\n        ];\n        const pluginsBlockedInTranslationMode = [\n            \"PowerboxPlugin\",\n            \"SearchPowerboxPlugin\",\n            \"MediaUrlPastePlugin\",\n            \"YoutubePlugin\",\n            \"ImagePlugin\",\n            \"AlignPlugin\",\n            \"ListPlugin\",\n            \"FontPlugin\",\n            \"FontFamilyPlugin\",\n        ];\n        const pluginsToRemove = this.props.translation\n            ? [...builderPluginsToRemove, ...pluginsBlockedInTranslationMode]\n            : builderPluginsToRemove;\n        const coreBuilderPlugins = removePlugins(\n            this.props.translation\n                ? [\n                      ...MAIN_PLUGINS,\n                      BuilderOverlayPlugin,\n                      OverlayButtonsPlugin,\n                      DropZonePlugin,\n                      DropZoneSelectorPlugin,\n                      CustomizeTabPlugin,\n                  ]\n                : CORE_PLUGINS,\n            pluginsToRemove\n        );\n        const Plugins = [...coreBuilderPlugins, ...(websitePlugins || [])];\n        builderProps.Plugins = Plugins;\n        builderProps.onEditorLoad = (editor) => {\n            this.editor = editor;\n        };\n        builderProps.config.getRecordInfo = (editableEl) => {\n            if (this.editor && !editableEl) {\n                editableEl = closestElement(\n                    this.editor.shared.selection.getEditableSelection().anchorNode,\n                    \"[data-oe-model]\"\n                );\n            }\n            if (!editableEl) {\n                return {};\n            }\n            return {\n                resModel: editableEl.dataset[\"oeModel\"],\n                resId: editableEl.dataset[\"oeId\"],\n                field: editableEl.dataset[\"oeField\"],\n                type: editableEl.dataset[\"oeType\"],\n            };\n        };\n        builderProps.getThemeTab = () => this.websiteService.isDesigner && ThemeTab;\n        builderProps.getCustomizeTranslationTab = () => CustomizeTranslationTab;\n        const installSnippetModule = builderProps.installSnippetModule;\n        builderProps.installSnippetModule = (snippet) =>\n            installSnippetModule(snippet, this.save.bind(this));\n        return builderProps;\n    }\n}\n\nregistry.category(\"lazy_components\").add(\"website.WebsiteBuilder\", WebsiteBuilder);\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { SNIPPET_SPECIFIC } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { renderToElement, renderToFragment } from \"@web/core/utils/render\";\n\nexport class DonationOption extends BaseOptionComponent {\n    static template = \"website_payment.DonationOption\";\n    static selector = \".s_donation\";\n    // TODO AGAU: remove when merging https://github.com/odoo-dev/odoo/pull/4240\n    static cleanForSave(editingElement) {\n        delete editingElement.dataset.prefilledOptionsList;\n    }\n}\n\nexport class DonationOptionPlugin extends Plugin {\n    static id = \"donationOption\";\n\n    resources = {\n        builder_options: [\n            withSequence(SNIPPET_SPECIFIC, DonationOption),\n        ],\n        builder_actions: {\n            ToggleDisplayOptionsAction,\n            TogglePrefilledOptionsAction,\n            ToggleDescriptionsAction,\n            SetPrefilledOptionsAction,\n            SelectAmountInputAction,\n            SetMinimumAmountAction,\n            SetMaximumAmountAction,\n            SetSliderStepAction,\n        },\n    };\n}\n\nexport class BaseDonationAction extends BuilderAction {\n    getPrefilledOptionsList({ editingElement }) {\n        const savedOptions = editingElement.dataset.prefilledOptionsList;\n\n        // TODO AGAU: remove when merging https://github.com/odoo-dev/odoo/pull/4240\n        {\n            if (savedOptions) {\n                return savedOptions;\n            } else {\n                const options = [];\n                const amounts = JSON.parse(editingElement.dataset.donationAmounts || \"[]\");\n                const descriptionEls = editingElement.querySelectorAll(\n                    \"#s_donation_description_inputs input\"\n                );\n                const descriptions = Array.from(descriptionEls).map(\n                    (descriptionEl) => descriptionEl.value\n                );\n                for (let i = 0; i < amounts.length; i++) {\n                    options.push({\n                        value: amounts[i],\n                        description:\n                            typeof descriptions[i] === \"string\"\n                                ? descriptions[i]\n                                : _t(\"Add a description here\"),\n                    });\n                }\n                return JSON.stringify(options);\n            }\n        }\n\n        // TODO AGAU: uncomment when merging https://github.com/odoo-dev/odoo/pull/4240\n        // return savedOptions || \"[]\";\n    }\n\n    rebuildPrefilledOptions(editingElement, options) {\n        if (!options) {\n            options = this.getPrefilledOptionsList({ editingElement });\n        }\n\n        // TODO AGAU: remove when merging https://github.com/odoo-dev/odoo/pull/4240\n        editingElement.dataset.prefilledOptionsList = options;\n\n        options = JSON.parse(options);\n\n        const displayOptions = editingElement.dataset.displayOptions;\n        const formEl = editingElement.querySelector(\".s_donation_form\");\n        const donateButtonEl = editingElement.querySelector(\".s_donation_donate_btn\");\n        const prefilledOptions = editingElement.dataset.prefilledOptions;\n        const showDescriptions = prefilledOptions && editingElement.dataset.descriptions;\n\n        // Slider\n        const layout = editingElement.dataset.customAmount;\n        const sliderEl = editingElement.querySelector(\".s_donation_range_slider_wrap\");\n        if (layout !== \"slider\" || !displayOptions) {\n            sliderEl?.remove();\n        } else if (layout === \"slider\" && displayOptions && !sliderEl) {\n            const sliderEl = renderToElement(\"website_payment.donation.slider\", {\n                minimum_amount: editingElement.dataset.minimumAmount,\n                maximum_amount: editingElement.dataset.maximumAmount,\n                slider_step: editingElement.dataset.sliderStep,\n            });\n            formEl.insertBefore(sliderEl, donateButtonEl);\n        }\n\n        // Hidden inputs for descriptions translation\n        const descriptionInputContainerEl = editingElement.querySelector(\n            \"#s_donation_description_inputs\"\n        );\n        descriptionInputContainerEl.textContent = \"\";\n        if (showDescriptions) {\n            descriptionInputContainerEl.insertBefore(\n                renderToFragment(\"website_payment.donation.descriptionTranslationInputs\", {\n                    descriptions: options.map((option) => option.description),\n                }),\n                null\n            );\n        }\n\n        // Displayed prefilled options\n        editingElement.querySelector(\".s_donation_prefilled_buttons\")?.remove();\n        if (displayOptions) {\n            // TODO AGAU: remove when merging https://github.com/odoo-dev/odoo/pull/4240\n            {\n                if (!showDescriptions) {\n                    options = options.map((option) => option.value);\n                }\n            }\n\n            const prefilledButtonsEl = renderToElement(\n                `website_payment.donation.prefilledButtons${\n                    showDescriptions ? \"Descriptions\" : \"\"\n                }`,\n                {\n                    prefilled_buttons: prefilledOptions ? options : [],\n                    custom_input: layout === \"freeAmount\",\n                    minimum_amount: editingElement.dataset.minimumAmount,\n                }\n            );\n            formEl.insertBefore(prefilledButtonsEl, descriptionInputContainerEl.nextSibling);\n        }\n    }\n}\n\nexport class ToggleDataAttributeAction extends BaseDonationAction {\n    /**\n     * @param {string} dataAttributeName - The data attribute to toggle (without \"data-\" prefix)\n     * @param {Function} toggleFunction - Function to call when applying or cleaning\n     */\n    setup(dataAttributeName, toggleFunction) {\n        this.dataAttributeName = dataAttributeName;\n        this.toggleFunction = toggleFunction;\n    }\n\n    /**\n     * Determine if the data attribute is applied.\n     *\n     * @param {Object} context\n     * @param {HTMLElement} context.editingElement\n     * @returns {boolean}\n     */\n    isApplied({ editingElement }) {\n        return !!editingElement.dataset[this.dataAttributeName];\n    }\n\n    /**\n     * Apply the data attribute and call the toggle function.\n     *\n     * @param {Object} context\n     * @param {HTMLElement} context.editingElement\n     * @param {...*} restArgs - Extra args for toggleFunction\n     */\n    apply(context, ...restArgs) {\n        const { editingElement } = context;\n        editingElement.dataset[this.dataAttributeName] = \"true\";\n        this.toggleFunction({ ...context, value: true }, ...restArgs);\n    }\n\n    /**\n     * Remove the data attribute and call the toggle function.\n     *\n     * @param {Object} context\n     * @param {HTMLElement} context.editingElement\n     * @param {...*} restArgs - Extra args for toggleFunction\n     */\n    clean(context, ...restArgs) {\n        const { editingElement } = context;\n        delete editingElement.dataset[this.dataAttributeName];\n        this.toggleFunction({ ...context, value: false }, ...restArgs);\n    }\n}\n\nexport class ToggleDisplayOptionsAction extends ToggleDataAttributeAction {\n    static id = \"toggleDisplayOptions\";\n\n    setup() {\n        super.setup(\"displayOptions\", this.toggleDisplayOptions);\n    }\n\n    toggleDisplayOptions({ editingElement, value }) {\n        if (!value && editingElement.dataset.customAmount === \"slider\") {\n            editingElement.dataset.customAmount = \"freeAmount\";\n        } else if (value && !editingElement.dataset.prefilledOptions) {\n            editingElement.dataset.customAmount = \"slider\";\n        }\n        this.rebuildPrefilledOptions(editingElement);\n    }\n}\n\nexport class TogglePrefilledOptionsAction extends ToggleDataAttributeAction {\n    static id = \"togglePrefilledOptions\";\n\n    setup() {\n        super.setup(\"prefilledOptions\", this.togglePrefilledOptions);\n    }\n\n    togglePrefilledOptions({ editingElement, value }) {\n        if (!value && editingElement.dataset.displayOptions) {\n            editingElement.dataset.customAmount = \"slider\";\n        }\n        this.rebuildPrefilledOptions(editingElement);\n    }\n}\n\nexport class ToggleDescriptionsAction extends ToggleDataAttributeAction {\n    static id = \"toggleDescriptions\";\n\n    setup() {\n        super.setup(\"descriptions\", ({ editingElement }) => {\n            this.rebuildPrefilledOptions(editingElement);\n        });\n    }\n}\n\nexport class SetPrefilledOptionsAction extends BaseDonationAction {\n    static id = \"setPrefilledOptions\";\n\n    getValue(context) {\n        return this.getPrefilledOptionsList(context);\n    }\n\n    apply({ editingElement, value }) {\n        // TODO AGAU: remove when merging https://github.com/odoo-dev/odoo/pull/4240\n        {\n            const options = JSON.parse(value);\n            const amounts = options.map((option) => option.value);\n            editingElement.dataset.donationAmounts = JSON.stringify(amounts);\n        }\n\n        editingElement.dataset.prefilledOptionsList = value;\n        this.rebuildPrefilledOptions(editingElement, value);\n    }\n}\n\nexport class SelectAmountInputAction extends BaseDonationAction {\n    static id = \"selectAmountInput\";\n\n    isApplied({ editingElement, params }) {\n        return editingElement.dataset.customAmount === params.mainParam;\n    }\n\n    apply({ editingElement, params }) {\n        editingElement.dataset.customAmount = params.mainParam;\n        this.rebuildPrefilledOptions(editingElement);\n    }\n}\n\nexport class SetMinimumAmountAction extends BuilderAction {\n    static id = \"setMinimumAmount\";\n\n    getValue({ editingElement }) {\n        return editingElement.dataset.minimumAmount;\n    }\n\n    apply({ editingElement, value }) {\n        editingElement.dataset.minimumAmount = value;\n        const rangeSliderEl = editingElement.querySelector(\"#s_donation_range_slider\");\n        const amountInputEl = editingElement.querySelector(\"#s_donation_amount_input\");\n        if (rangeSliderEl) {\n            rangeSliderEl.min = value;\n        } else if (amountInputEl) {\n            amountInputEl.min = value;\n        }\n    }\n}\n\nexport class SetMaximumAmountAction extends BuilderAction {\n    static id = \"setMaximumAmount\";\n\n    getValue({ editingElement }) {\n        return editingElement.dataset.maximumAmount;\n    }\n\n    apply({ editingElement, value }) {\n        editingElement.dataset.maximumAmount = value;\n        const rangeSliderEl = editingElement.querySelector(\"#s_donation_range_slider\");\n        const amountInputEl = editingElement.querySelector(\"#s_donation_amount_input\");\n        if (rangeSliderEl) {\n            rangeSliderEl.max = value;\n        } else if (amountInputEl) {\n            amountInputEl.max = value;\n        }\n    }\n}\n\nexport class SetSliderStepAction extends BuilderAction {\n    static id = \"setSliderStep\";\n\n    getValue({ editingElement }) {\n        return editingElement.dataset.sliderStep;\n    }\n\n    apply({ editingElement, value }) {\n        editingElement.dataset.sliderStep = value;\n        const rangeSliderEl = editingElement.querySelector(\"#s_donation_range_slider\");\n        if (rangeSliderEl) {\n            rangeSliderEl.step = value;\n        }\n    }\n}\n\nregistry.category(\"website-plugins\").add(DonationOptionPlugin.id, DonationOptionPlugin);\n", "import { BuilderAction } from '@html_builder/core/builder_action';\nimport { BaseOptionComponent } from '@html_builder/core/utils';\nimport { SNIPPET_SPECIFIC } from '@html_builder/utils/option_sequence';\nimport { Plugin } from '@html_editor/plugin';\nimport { withSequence } from '@html_editor/utils/resource';\nimport { _t } from '@web/core/l10n/translation';\nimport { registry } from '@web/core/registry';\n\n\nexport class SupportedPaymentMethodsOption extends BaseOptionComponent {\n    static template = 'website_payment.SupportedPaymentMethodsOption';\n    static selector = '.s_supported_payment_methods';\n}\n\nclass SupportedPaymentMethodsOptionPlugin extends Plugin {\n    static id = 'supportedPaymentMethodsOption';\n    static dependencies = ['edit_interaction'];\n    resources = {\n        so_content_addition_selector: ['.s_supported_payment_methods'],\n        builder_options: [\n            withSequence(SNIPPET_SPECIFIC, SupportedPaymentMethodsOption),\n        ],\n        builder_actions: { SupportedPaymentMethodsLimit, SupportedPaymentMethodsHeight },\n        get_options_container_top_buttons: withSequence(0, this.getOptionButtons.bind(this)),\n        get_overlay_buttons: withSequence(0, { getButtons: this.getOptionButtons.bind(this) }),\n    };\n\n    /**\n     * Add a reload button at the top in case the user made some changes to the supported payment\n     * methods. This only reloads the snippet element and not the entire editor page.\n     */\n    getOptionButtons(editingElement) {\n        if (editingElement.dataset.snippet !== 's_supported_payment_methods') {\n            return [];\n        }\n        return [{\n            class: 'fa fa-fw fa-rotate-right btn btn-outline-info',\n            title: _t(\"Reload the payment methods\"),\n            // Force the interaction to call the server again in case the user made backend changes.\n            handler: () => this.dependencies.edit_interaction.restartInteractions(editingElement),\n        }];\n    }\n}\n\n\nclass SupportedPaymentMethodsLimit extends BuilderAction {\n    static id = 'supportedPaymentMethodsLimit';\n\n    getValue({ editingElement }) {\n        return editingElement.dataset.limit;\n    }\n\n    apply({ editingElement, value }) {\n        editingElement.dataset.limit = value;\n    }\n}\n\n\nclass SupportedPaymentMethodsHeight extends BuilderAction {\n    static id = 'supportedPaymentMethodsHeight';\n\n    getValue({ editingElement }) {\n        return editingElement.dataset.height;\n    }\n\n    apply({ editingElement, value }) {\n        editingElement.dataset.height = value;\n    }\n}\n\n\nregistry\n    .category('website-plugins')\n    .add(SupportedPaymentMethodsOptionPlugin.id, SupportedPaymentMethodsOptionPlugin);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nregistry.category(\"website.form_editor_actions\").add(\"create_customer\", {\n    formFields: [\n        {\n            type: \"char\",\n            modelRequired: true,\n            name: \"name\",\n            fillWith: \"name\",\n            string: _t(\"Your Name\"),\n        },\n        {\n            type: \"email\",\n            required: true,\n            fillWith: \"email\",\n            name: \"email\",\n            string: _t(\"Your Email\"),\n        },\n        {\n            type: \"tel\",\n            fillWith: \"phone\",\n            name: \"phone\",\n            string: _t(\"Phone Number\"),\n        },\n        {\n            type: \"char\",\n            name: \"company_name\",\n            fillWith: \"commercial_company_name\",\n            string: _t(\"Company Name\"),\n        },\n    ],\n});\n", "import { BaseOptionComponent, useDomState, useGetItemValue } from \"@html_builder/core/utils\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport const addToCartValues = {\n    addToCart: { action: \"add_to_cart\", icon: \"fa-cart-plus\", label: _t(\"Add to Cart\") },\n    buyNow: { action: \"buy_now\", icon: \"fa-credit-card\", label: _t(\"Buy Now\") },\n};\n\nexport class AddToCartOption extends BaseOptionComponent {\n    static template = \"website_sale.AddToCartOption\";\n    static selector = \".s_add_to_cart\";\n    static props = [];\n    setup() {\n        super.setup();\n        this.getItemValue = useGetItemValue();\n        this.domState = useDomState((editingElement) => ({\n            shouldShowActionChoice:\n                editingElement.dataset.variants?.split(\",\").length === 1 ||\n                !!editingElement.dataset.productVariant,\n        }));\n        this.addToCartValues = addToCartValues;\n    }\n\n    getItemValueJSON(id) {\n        const value = this.getItemValue(id);\n        return value && JSON.parse(value);\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { AddToCartOption, addToCartValues } from \"./add_to_card_option\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\n\nclass AddToCartOptionPlugin extends Plugin {\n    static id = \"addToCartOption\";\n    static dependencies = [\"builderActions\"];\n    resources = {\n        builder_options: [AddToCartOption],\n        so_content_addition_selector: [\".s_add_to_cart\"],\n        builder_actions: {\n            ProductToCartAction,\n            VariantToCartAction,\n            AddToCartActionAction,\n        },\n    };\n}\n\nexport class ProductToCartAction extends BuilderAction {\n    static id = \"productToCart\";\n    static dependencies = [\"builderActions\"];\n    apply({ editingElement, value }) {\n        const classAction = this.dependencies.builderActions.getAction(\"classAction\");\n\n        const { id, type, product_variant_ids } = JSON.parse(value);\n\n        editingElement.dataset.productTemplate = id;\n        editingElement.dataset.productType = type;\n        editingElement.dataset.variants = product_variant_ids.join(\",\");\n        delete editingElement.dataset.productVariant;\n\n        const buttonEl = editingElement.querySelector(\".s_add_to_cart_btn\");\n        buttonEl.dataset.productTemplateId = id;\n        buttonEl.dataset.productType = type;\n        const oneVariant = product_variant_ids.length === 1;\n        if (oneVariant) {\n            buttonEl.dataset.productVariantId = product_variant_ids[0];\n        } else {\n            delete buttonEl.dataset.productVariantId;\n        }\n        classAction.clean({\n            editingElement: buttonEl,\n            params: { mainParam: \"disabled\" },\n        });\n        if (!oneVariant) {\n            this.dependencies.builderActions\n                .getAction(\"addToCartAction\")\n                .resetDefaultAction(editingElement);\n        }\n    }\n    clean({ editingElement }) {\n        const classAction = this.dependencies.builderActions.getAction(\"classAction\");\n        delete editingElement.dataset.productTemplate;\n        delete editingElement.dataset.productType;\n        delete editingElement.dataset.variants;\n        delete editingElement.dataset.productVariant;\n        const buttonEl = editingElement.querySelector(\".s_add_to_cart_btn\");\n        delete buttonEl.dataset.productTemplateId;\n        delete buttonEl.dataset.productType;\n        delete buttonEl.dataset.productVariantId;\n        classAction.apply({\n            editingElement: buttonEl,\n            params: { mainParam: \"disabled\" },\n        });\n        this.dependencies.builderActions\n            .getAction(\"addToCartAction\")\n            .resetDefaultAction(editingElement);\n    }\n    getValue({ editingElement }) {\n        const value = {};\n        const id = editingElement.dataset.productTemplate;\n        if (!id) {\n            return;\n        }\n        value.id = parseInt(id);\n        const type = editingElement.dataset.productType;\n        if (type !== undefined) {\n            value.type = type;\n        }\n        const product_variant_ids = editingElement.dataset.variants\n            ?.split(\",\")\n            .map((el) => parseInt(el));\n        if (product_variant_ids !== undefined) {\n            value.product_variant_ids = product_variant_ids;\n        }\n        return JSON.stringify(value);\n    }\n}\nexport class VariantToCartAction extends BuilderAction {\n    static id = \"variantToCart\";\n    static dependencies = [\"builderActions\"];\n    apply({ editingElement, value }) {\n        const { id } = JSON.parse(value);\n        editingElement.dataset.productVariant = id;\n        const buttonEl = editingElement.querySelector(\".s_add_to_cart_btn\");\n        buttonEl.dataset.productVariantId = id;\n    }\n    clean({ editingElement }) {\n        delete editingElement.dataset.productVariant;\n        const buttonEl = editingElement.querySelector(\".s_add_to_cart_btn\");\n        delete buttonEl.dataset.productVariantId;\n        this.dependencies.builderActions\n            .getAction(\"addToCartAction\")\n            .resetDefaultAction(editingElement);\n    }\n    getValue({ editingElement }) {\n        const id = editingElement.dataset.productVariant;\n        if (id) {\n            return JSON.stringify({ id: parseInt(id) });\n        }\n    }\n}\nexport class AddToCartActionAction extends BuilderAction {\n    static id = \"addToCartAction\";\n    static dependencies = [\"builderActions\"];\n    apply({ editingElement, params: { action, icon, label } }) {\n        const classAction = this.dependencies.builderActions.getAction(\"classAction\");\n        editingElement.dataset.action = action;\n        const buttonEl = editingElement.querySelector(\".s_add_to_cart_btn\");\n        buttonEl.dataset.action = action;\n        const iconEl = buttonEl.querySelector(\"i\");\n        classAction.apply({\n            editingElement: iconEl,\n            params: { mainParam: icon },\n        });\n        buttonEl.lastChild.textContent = label;\n    }\n    clean({ editingElement, params: { icon } }) {\n        const classAction = this.dependencies.builderActions.getAction(\"classAction\");\n\n        delete editingElement.dataset.action;\n        const buttonEl = editingElement.querySelector(\".s_add_to_cart_btn\");\n        delete buttonEl.dataset.action;\n        const iconEl = buttonEl.querySelector(\"i\");\n        classAction.clean({\n            editingElement: iconEl,\n            params: { mainParam: icon },\n        });\n    }\n    isApplied({ editingElement, params: { action } }) {\n        return editingElement.dataset.action === action;\n    }\n\n    resetDefaultAction(editingElement) {\n        if (this.isApplied({ editingElement, params: addToCartValues.buyNow })) {\n            this.clean({ editingElement, params: addToCartValues.buyNow });\n            this.apply({ editingElement, params: addToCartValues.addToCart });\n        }\n    }\n}\n\nregistry.category(\"website-plugins\").add(AddToCartOptionPlugin.id, AddToCartOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { WebsiteConfigAction } from \"@website/builder/plugins/customize_website_plugin\";\n\nexport class CheckoutPageOption extends BaseOptionComponent {\n    static template = \"website_sale.checkoutPageOption\";\n    static selector = \"main:has(.oe_website_sale .o_wizard)\";\n    static title = _t(\"Checkout Pages\");\n    static groups = [\"website.group_website_designer\"];\n    static editableOnly = false;\n}\n\nclass CheckoutPageOptionPlugin extends Plugin {\n    static id = \"checkoutPageOption\";\n    resources = {\n        builder_options: [CheckoutPageOption],\n        builder_actions: {\n            SetExtraStepAction,\n        },\n    };\n}\n\nexport class SetExtraStepAction extends WebsiteConfigAction {\n    static id = \"setExtraStep\";\n    async apply(context) {\n        await Promise.all([\n            super.apply(context),\n            rpc(\"/shop/config/website\", { extra_step: \"true\" }),\n        ]);\n    }\n    async clean(context) {\n        await Promise.all([\n            super.clean(context),\n            rpc(\"/shop/config/website\", { extra_step: \"false\" }),\n        ]);\n    }\n}\n\nregistry.category(\"website-plugins\").add(CheckoutPageOptionPlugin.id, CheckoutPageOptionPlugin);\n", "import { BuilderAction } from '@html_builder/core/builder_action';\nimport { BaseOptionComponent } from '@html_builder/core/utils';\nimport { Plugin } from '@html_editor/plugin';\nimport { _t } from '@web/core/l10n/translation';\nimport { rpc } from '@web/core/network/rpc';\nimport { registry } from '@web/core/registry';\n\nexport class DynamicSnippetCategoryItemOptions extends BaseOptionComponent {\n    static template = 'website_sale.dynamicSnippetCategoryItemOptions';\n    static selector = '.s_dynamic_category_item, .all_products';\n    static title = _t(\"Category\");\n    static editableOnly = false;\n}\n\nexport class DynamicSnippetCategoryItemOptionPlugin extends Plugin {\n    static id = 'dynamicSnippetCategoryItemOptionPlugin';\n    resources = {\n        builder_options: DynamicSnippetCategoryItemOptions,\n        builder_actions: { SetCategoryImageAction },\n    }\n}\n\nclass SetCategoryImageAction extends BuilderAction {\n    static id = 'setCategoryImage';\n    static dependencies = ['media', 'builderOptions'];\n\n    async apply({ editingElement: el }) {\n        const categoryId = el.dataset.categoryId;\n        const categoryImage = el.querySelector('[name=\"category_image\"]');\n        if (!categoryImage) return;\n        await this.dependencies.media.openMediaDialog({\n            node: categoryImage,\n            onlyImages: true,\n            noDocuments: true,\n            save: async (selectedImageEl, selectedMedia) => {\n                rpc('/snippets/category/set_image', {\n                    category_id: parseInt(categoryId),\n                    attachment_id: selectedMedia[0]['id'],\n                });\n                if (!(selectedImageEl instanceof HTMLImageElement)) return;\n                categoryImage.replaceWith(selectedImageEl);\n                this.dependencies['builderOptions'].updateContainers(selectedImageEl);\n            },\n        });\n    }\n}\n\nregistry.category('website-plugins').add(\n    DynamicSnippetCategoryItemOptionPlugin.id, DynamicSnippetCategoryItemOptionPlugin,\n);\n", "import { Plugin } from '@html_editor/plugin';\nimport { withSequence } from '@html_editor/utils/resource';\nimport { BuilderAction } from '@html_builder/core/builder_action';\nimport { registry } from '@web/core/registry';\nimport { DEVICE_VISIBILITY } from '@website/builder/option_sequence';\nimport {\n    setDatasetIfUndefined\n} from '@website/builder/plugins/options/dynamic_snippet_option_plugin';\nimport { DynamicSnippetCategoryOption } from './dynamic_snippet_category_options';\n\nconst TEMPLATE_OPTIONS = {\n    'clickable': 'website_sale.dynamic_filter_template_product_public_category_clickable_items',\n    'default': 'website_sale.dynamic_filter_template_product_public_category_default',\n}\n\nconst modelNameFilter = 'product.public.category';\n\nexport class DynamicSnippetCategoryOption2 extends DynamicSnippetCategoryOption {\n    static selector = 'section.s_dynamic_snippet_category';\n    static defaultProps = {\n        modelNameFilter,\n    };\n    static groups = ['website.group_website_designer'];\n}\n\nexport class DynamicSnippetCategoryOptionPlugin extends Plugin {\n    static id = 'dynamicSnippetCategoryOptionPlugin';\n    static dependencies = ['dynamicSnippetOption'];\n    selector = DynamicSnippetCategoryOption2.selector;\n    modelNameFilter = modelNameFilter;\n    resources = {\n        builder_options: [\n            withSequence(DEVICE_VISIBILITY, DynamicSnippetCategoryOption2),\n        ],\n        builder_actions: { ToggleClickableAction },\n        on_snippet_dropped_handlers: this.onSnippetDropped.bind(this),\n    };\n\n    async onSnippetDropped({ snippetEl }) {\n        if (snippetEl.matches(this.selector)) {\n            for (const [optionName, value] of [\n                ['showParent', true],\n                ['columns', 4],\n                ['rounded', 2],\n                ['gap', 2],\n                ['size', 'medium'],\n                ['alignment', 'center'],\n            ]) {\n                setDatasetIfUndefined(snippetEl, optionName, value);\n            }\n            await this.dependencies.dynamicSnippetOption.setOptionsDefaultValues(\n                snippetEl,\n                this.modelNameFilter,\n                [],\n            );\n        }\n    }\n}\n\nexport class ToggleClickableAction extends BuilderAction {\n    static id = 'toggleClickable';\n    apply({ editingElement }) {\n        const nodeData = editingElement.dataset;\n        nodeData.templateKey = nodeData.templateKey === TEMPLATE_OPTIONS['default']\n            ? TEMPLATE_OPTIONS['clickable']\n            : TEMPLATE_OPTIONS['default'];\n    }\n}\n\nregistry.category('website-plugins').add(\n    DynamicSnippetCategoryOptionPlugin.id, DynamicSnippetCategoryOptionPlugin,\n);\n", "import { useDomState } from '@html_builder/core/utils';\nimport { onWillStart } from '@odoo/owl';\nimport { useService } from '@web/core/utils/hooks';\nimport { DynamicSnippetOption } from '@website/builder/plugins/options/dynamic_snippet_option';\n\n\nexport class DynamicSnippetCategoryOption extends DynamicSnippetOption {\n    static template = 'website_sale.DynamicSnippetCategoryOptions';\n\n    setup() {\n        super.setup();\n        this.orm = useService('orm');\n        this.website = useService('website');\n        this.dynamicOptionParams.domState = useDomState(editingElement => ({\n            parentCategoryId: editingElement.dataset.parentCategoryId,\n        }))\n        this.categories = [];\n\n        onWillStart(async () => {\n            this.categories = await this.orm.call(\n                'product.public.category',\n                'get_available_snippet_categories',\n                [this.website.currentWebsiteId],\n            );\n        });\n    }\n}\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { useDynamicSnippetOption } from \"@website/builder/plugins/options/dynamic_snippet_hook\";\nimport { onWillStart, useState } from \"@odoo/owl\";\n\nexport class DynamicSnippetProductsOption extends BaseOptionComponent {\n    static template = \"website_sale.DynamicSnippetProductsOption\";\n    static dependencies = [\"dynamicSnippetProductsOption\"];\n    static selector = \".s_dynamic_snippet_products\";\n    setup() {\n        super.setup();\n        const { fetchCategories, getModelNameFilter } = this.dependencies.dynamicSnippetProductsOption;\n        const contextualFilterDomain = getContextualFilterDomain(this.env.editor.editable);\n        this.dynamicOptionParams = useDynamicSnippetOption(\n            getModelNameFilter(),\n            contextualFilterDomain\n        );\n        this.state = useState({\n            categories: [],\n        });\n        this.domState = useDomState((el) => ({\n            isAlternative: el.classList.contains(\"o_wsale_alternative_products\"),\n        }));\n        this.dynamicOptionParams.showFilterOption = () =>\n            Object.values(this.dynamicOptionParams.dynamicFilters).length > 1 &&\n            !this.domState.isAlternative;\n        onWillStart(async () => {\n            this.state.categories.push(...(await fetchCategories()));\n        });\n    }\n}\n\nexport function getContextualFilterDomain(editable) {\n    const productTemplateId = editable.querySelector(\"input.product_template_id\");\n    const hasProductTemplateId = productTemplateId?.value;\n    return hasProductTemplateId ? [] : [[\"product_cross_selling\", \"=\", false]];\n}\n", "import { DYNAMIC_SNIPPET_CAROUSEL } from \"@website/builder/plugins/options/dynamic_snippet_carousel_option_plugin\";\nimport { setDatasetIfUndefined } from \"@website/builder/plugins/options/dynamic_snippet_option_plugin\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\nimport {\n    DynamicSnippetProductsOption,\n    getContextualFilterDomain,\n} from \"./dynamic_snippet_products_option\";\n\nclass DynamicSnippetProductsOptionPlugin extends Plugin {\n    static id = \"dynamicSnippetProductsOption\";\n    static dependencies = [\"dynamicSnippetCarouselOption\"];\n    static shared = [\"fetchCategories\", \"getModelNameFilter\"];\n    modelNameFilter = \"product.product\";\n    resources = {\n        builder_options: withSequence(DYNAMIC_SNIPPET_CAROUSEL, DynamicSnippetProductsOption),\n        dynamic_snippet_template_updated: this.onTemplateUpdated.bind(this),\n        on_snippet_dropped_handlers: this.onSnippetDropped.bind(this),\n    };\n    setup() {\n        this.categories = undefined;\n    }\n    destroy() {\n        super.destroy();\n        this.categories = undefined;\n    }\n    async onSnippetDropped({ snippetEl }) {\n        if (snippetEl.matches(DynamicSnippetProductsOption.selector)) {\n            for (const [optionName, value] of [\n                [\"productCategoryId\", \"all\"],\n                [\"showVariants\", true],\n            ]) {\n                setDatasetIfUndefined(snippetEl, optionName, value);\n            }\n            await this.dependencies.dynamicSnippetCarouselOption.setOptionsDefaultValues(\n                snippetEl,\n                this.modelNameFilter,\n                getContextualFilterDomain(this.editable)\n            );\n        }\n    }\n    getModelNameFilter() {\n        return this.modelNameFilter;\n    }\n    onTemplateUpdated({ el, template }) {\n        if (el.matches(DynamicSnippetProductsOption.selector)) {\n            this.dependencies.dynamicSnippetCarouselOption.updateTemplateSnippetCarousel(\n                el,\n                template\n            );\n        }\n    }\n    async fetchCategories() {\n        if (!this.categories) {\n            this.categories = this._fetchCategories();\n        }\n        return this.categories;\n    }\n    async _fetchCategories() {\n        // TODO put in an utility function\n        const websiteDomain = [\n            \"|\",\n            [\"website_id\", \"=\", false],\n            [\"website_id\", \"=\", this.services.website.currentWebsite.id],\n        ];\n        return this.services.orm.searchRead(\n            \"product.public.category\",\n            websiteDomain,\n            [\"id\", \"name\"],\n            { order: \"name asc\" }\n        );\n    }\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(DynamicSnippetProductsOptionPlugin.id, DynamicSnippetProductsOptionPlugin);\n", "import { onWillStart } from \"@odoo/owl\";\nimport { MegaMenuOption } from \"@website/builder/plugins/options/mega_menu_option\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(MegaMenuOption.prototype, {\n    setup() {\n        this.orm = useService(\"orm\");\n        this.website = useService('website');\n        super.setup();\n        this.productCategories = [];\n\n        onWillStart(async () => {\n            this.productCategories = await this.orm.call(\"product.public.category\", \"search\", [[\n                \"|\",\n                [\"website_id\", \"=\", false],\n                [\"website_id\", \"=\", this.website.currentWebsiteId],\n            ]]);\n        });\n    },\n});\n", "import { MegaMenuOptionPlugin } from \"@website/builder/plugins/options/mega_menu_option_plugin\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\n\npatch(MegaMenuOptionPlugin.prototype, {\n    getTemplatePrefix(editingEl, toggle) {\n        const hasSaleClass = editingEl.classList.contains(\"fetchEcomCategories\");\n        const fetchWebsiteSale = toggle ? !hasSaleClass : hasSaleClass;\n        if (fetchWebsiteSale) {\n            return \"website_sale.\";\n        }\n        return super.getTemplatePrefix(editingEl);\n    },\n});\n\nclass WebsiteSaleMegaMenuOptionPlugin extends Plugin {\n    static id = \"websiteSaleMegaMenuOptionPlugin\";\n    static dependencies = [\n        \"builderOptions\",\n        \"customizeWebsite\",\n        \"history\",\n        \"megaMenuOptionPlugin\",\n    ];\n    resources = {\n        builder_actions: {\n            ToggleFetchEcomCategoriesAction,\n        },\n    };\n}\n\nexport class ToggleFetchEcomCategoriesAction extends BuilderAction {\n    static id = \"toggleFetchEcomCategories\";\n    static dependencies = [\"megaMenuOptionPlugin\", \"customizeWebsite\"];\n    async load({ editingElement }) {\n        const module = this.dependencies.megaMenuOptionPlugin.getTemplatePrefix(\n            editingElement,\n            true\n        );\n        const cls = [...editingElement.querySelector(\"section\").classList].find((cls) =>\n            cls.startsWith(\"s_mega_menu_\")\n        );\n        const templateKey = `${module}${cls}`;\n        await this.dependencies.customizeWebsite.loadTemplateKey(templateKey);\n        return templateKey;\n    }\n    apply({ editingElement, loadResult }) {\n        this.dependencies.customizeWebsite.toggleTemplate(\n            {\n                editingElement,\n                params: { view: loadResult },\n            },\n            true\n        );\n    }\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(WebsiteSaleMegaMenuOptionPlugin.id, WebsiteSaleMegaMenuOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class ProductAttributeOption extends BaseOptionComponent {\n    static template = \"website_sale.ProductAttributeOption\";\n    static selector = \"#product_detail .o_wsale_product_attribute\";\n    static editableOnly = false;\n    static reloadTarget = true;\n}\n\nclass ProductAttributeOptionPlugin extends Plugin {\n    static id = \"productAttributeOption\";\n    resources = {\n        builder_options: ProductAttributeOption,\n        builder_actions: {\n            ProductAttributeDisplayAction,\n        },\n    };\n\n}\n\nexport class ProductAttributeDisplayAction extends BuilderAction {\n    static id = \"productAttributeDisplay\";\n\n    setup() {\n        this.reload = {};\n    }\n    isApplied({ editingElement: el, value }) {\n        return value === this.getProductAttributeDisplay(el);\n    }\n    getValue({ editingElement: el }) {\n        return this.getProductAttributeDisplay(el);\n    }\n    async apply({ editingElement: el, value }) {\n        const attributeID = parseInt(\n            el.closest(\"[data-attribute-id]\").dataset.attributeId\n        );\n        await rpc(\"/shop/config/attribute\", {\n            attribute_id: attributeID,\n            display_type: value,\n        });\n    }\n    getProductAttributeDisplay(el) {\n        return el.closest(\"[data-attribute-display-type]\").dataset.attributeDisplayType;\n    }\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(ProductAttributeOptionPlugin.id, ProductAttributeOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class ProductHeaderCategoryOption extends BaseOptionComponent {\n    static template = \"website_sale.ProductHeaderCategoryOption\";\n    static selector = \"#products_grid:has(header.o_wsale_products_header_is_category)\";\n    static editableOnly = false;\n    static reloadTarget = true;\n    static getSnippetTitle() {\n        return _t((this.editable.querySelector(\"#o_wsale_products_header\")?.dataset.categoryName || \"Category\") + ' Header');\n    };\n    static groups = [\"website.group_website_restricted_editor\"];\n}\n\nclass ProductHeaderCategoryOptionPlugin extends Plugin {\n    static id = \"ProductHeaderCategoryOptionPlugin\";\n\n    resources = {\n        builder_options: ProductHeaderCategoryOption,\n        builder_actions: {\n            ToggleCategoryShowTitleAction,\n            ToggleCategoryShowDescriptionAction,\n            ToggleCategoryAlignContentAction,\n        },\n\n        save_handlers: this.onSave.bind(this),\n    };\n\n    async onSave() {\n        const headerEl = this.editable.querySelector(\"#o_wsale_products_header\");\n        if (!headerEl) return;\n        const categoryId = headerEl.dataset.categoryId;\n\n        const showTitle = headerEl.classList.contains(\"o_wsale_products_header_show_category_title\");\n        const showDescription = headerEl.classList.contains(\"o_wsale_products_header_show_category_description\");\n        const alignCategoryContent = headerEl.classList.contains(\"o_wsale_products_header_category_center_content\");\n\n        if (categoryId) {\n            return rpc(\"/shop/config/category\", {\n                category_id: categoryId,\n                show_category_title: showTitle,\n                show_category_description: showDescription,\n                align_category_content: alignCategoryContent,\n            });\n        }\n    }\n}\n\nclass BaseCategoryToggleAction extends BuilderAction {\n    isApplied({ editingElement: el, params }) {\n        return el.classList.contains(params.previewClass);\n    }\n\n    apply({ editingElement: el, params }) {\n        el.classList.add(params.previewClass);\n    }\n\n    clean({ editingElement: el, params }) {\n        el.classList.remove(params.previewClass);\n    }\n}\n\nexport class ToggleCategoryShowTitleAction extends BaseCategoryToggleAction {\n    static id = \"toggleCategoryShowTitle\";\n}\n\nexport class ToggleCategoryShowDescriptionAction extends BaseCategoryToggleAction {\n    static id = \"toggleCategoryShowDescription\";\n}\n\nexport class ToggleCategoryAlignContentAction extends BaseCategoryToggleAction {\n    static id = \"toggleCategoryAlignContent\";\n}\n\nregistry.category(\"website-plugins\").add(ProductHeaderCategoryOptionPlugin.id, ProductHeaderCategoryOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class ProductHeaderShopOption extends BaseOptionComponent {\n    static template = \"website_sale.ProductHeaderShopOption\";\n    static selector = \"#products_grid:has(header.o_wsale_products_header_is_shop)\";\n    static editableOnly = false;\n    static reloadTarget = true;\n    static title = _t(\"Shop Header\");\n}\n\nclass ProductHeaderShopOptionPlugin extends Plugin {\n    static id = \"ProductHeaderShopOptionPlugin\";\n\n    resources = {\n        builder_options: ProductHeaderShopOption,\n    };\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(ProductHeaderShopOptionPlugin.id, ProductHeaderShopOptionPlugin);\n", "import { REPLACE_MEDIA } from \"@html_builder/utils/option_sequence\";\nimport {\n    ReplaceMediaOption,\n} from \"@html_builder/plugins/image/replace_media_option\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\n\nexport class ProductImageOption extends BaseOptionComponent {\n    static template = \"website_sale.ProductImageOption\";\n    static selector =  `.o_wsale_product_images :is(${ReplaceMediaOption.selector})`;\n    static exclude = ReplaceMediaOption.exclude;\n}\n\nexport class ProductImageOptionPlugin extends Plugin {\n    static id = \"productImageOption\";\n    resources = {\n        builder_options: [\n            withSequence(REPLACE_MEDIA, ProductImageOption),\n        ],\n        builder_actions: {\n            /*\n             * Change sequence of product page images\n             */\n            SetPositionAction,\n            /*\n             * Removes the image in the back-end\n             */\n            RemoveMediaAction,\n        },\n        patch_builder_options: [\n            {\n                target_name: \"replaceMediaOption\",\n                target_element: \"exclude\",\n                method: \"add\",\n                value: ProductImageOption.selector,\n            },\n        ],\n    };\n}\n\n/*\n* Change sequence of product page images\n*/\nexport class SetPositionAction extends BuilderAction {\n    static id = \"setPosition\";\n    setup() {\n        this.reload = {};\n    }\n    async apply({ editingElement: el, value }) {\n        const params = {\n            image_res_model: el.parentElement.dataset.oeModel,\n            image_res_id: el.parentElement.dataset.oeId,\n            move: value,\n        };\n\n        await rpc(\"/shop/product/resequence-image\", params);\n    }\n}\n/*\n * Removes the image in the back-end\n */\nexport class RemoveMediaAction extends BuilderAction {\n    static id = \"removeMedia\";\n    setup() {\n        this.reload = {};\n    }\n    async apply({ editingElement: el }) {\n        if (el.parentElement.dataset.oeModel === \"product.image\") {\n            // Unlink the \"product.image\" record as it is not the main product image.\n            await this.services.orm.unlink(\"product.image\", [\n                parseInt(el.parentElement.dataset.oeId),\n            ]);\n        }\n        el.remove();\n    }\n}\n\nregistry.category(\"website-plugins\").add(ProductImageOptionPlugin.id, ProductImageOptionPlugin);\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ProductPageOption extends BaseOptionComponent {\n    static template = \"website_sale.ProductPageOption\";\n    static dependencies = [\"productPageOption\"];\n    static selector = \"main:has(.o_wsale_product_page)\";\n    static title = _t(\"Product Page\");\n    static editableOnly = false;\n\n    setup() {\n        super.setup();\n        this.domState = useDomState((el) => {\n            const productDetailEl = el.querySelector(\"#product_detail\");\n            const productDetailMainEl = el.querySelector(\"#product_detail_main\");\n            const productPageCarouselEl = el.querySelector(\"#o-carousel-product\");\n            const productPageGridEl = el.querySelector(\"#o-grid-product\");\n            const hasImages = !productDetailEl.classList.contains(\n                \"o_wsale_product_page_opt_image_width_none\"\n            );\n            const isFullImage = productDetailEl.classList.contains(\n                \"o_wsale_product_page_opt_image_width_100_pc\"\n            );\n            const multipleImages =\n                hasImages &&\n                productDetailMainEl.querySelector(\".o_wsale_product_images\")?.dataset.imageAmount >\n                    1;\n            const isGrid = !!productDetailMainEl.querySelector(\"#o-grid-product\");\n            const hasCarousel = !!productPageCarouselEl;\n            const hasGrid = !!productPageGridEl;\n            return {\n                hasImages,\n                isFullImage,\n                multipleImages,\n                isGrid,\n                hasCarousel,\n                hasGrid,\n            };\n        });\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { ProductPageOption } from \"./product_page_option\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { isImageCorsProtected } from \"@html_editor/utils/image\";\nimport { TABS } from \"@html_editor/main/media/media_dialog/media_dialog\";\nimport { WebsiteConfigAction, PreviewableWebsiteConfigAction } from \"@website/builder/plugins/customize_website_plugin\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport wSaleUtils from \"@website_sale/js/website_sale_utils\";\n\nclass ProductPageOptionPlugin extends Plugin {\n    static id = \"productPageOption\";\n    static dependencies = [\"builderActions\", \"media\", \"customizeWebsite\"];\n    static shared = [\"forceCarouselRedraw\"];\n    resources = {\n        builder_options: ProductPageOption,\n        builder_actions: {\n            ProductPageContainerWidthAction,\n            ProductPageContainerOrderAction,\n            ProductPageImageWidthAction,\n            ProductPageImageRatioAction,\n            ProductPageImageRatioMobileAction,\n            ProductPageImageLayoutAction,\n            ProductPageImageRoundnessAction,\n            ProductPageImageGridSpacingAction,\n            ProductPageImageGridColumnsAction,\n            ProductReplaceMainImageAction,\n            ProductAddExtraImageAction,\n            ProductRemoveAllExtraImagesAction,\n        },\n        clean_for_save_handlers: ({ root: el }) => {\n            // TODO the content of this clean_for_save_handlers should probably\n            // be a generic thing for the whole editor.\n\n            // Make sure that if the user removes the whole text of the\n            // breadcrumb, it is restored to the default value.\n            if (\n                // TODO the \"placeholder\" feature should be reviewed, this is\n                // not a valid HTML attribute.\n                el.getAttribute(\"placeholder\")\n                && el.hasAttribute(\"data-oe-zws-empty-inline\")\n                && /^[\\s\\u200b]*$/.test(el.textContent)\n            ) {\n                el.textContent = el.getAttribute(\"placeholder\");\n            }\n\n            const mainEl = el.querySelector(ProductPageOption.selector);\n            if (!mainEl) {\n                return;\n            }\n            const productDetailMain = mainEl.querySelector(\"#product_detail_main\");\n            if (!productDetailMain) {\n                return;\n            }\n            const accordionEl = productDetailMain.querySelector(\"#product_accordion\");\n            if (!accordionEl) {\n                return;\n            }\n\n            const accordionItemsEls = accordionEl.querySelectorAll(\".accordion-item\");\n            accordionItemsEls.forEach((item, key) => {\n                const accordionButtonEl = item.querySelector(\".accordion-button\");\n                const accordionCollapseEl = item.querySelector(\".accordion-collapse\");\n                if (key !== 0 && accordionCollapseEl.classList.contains(\"show\")) {\n                    accordionButtonEl.classList.add(\"collapsed\");\n                    accordionButtonEl.setAttribute(\"aria-expanded\", \"false\");\n                    accordionCollapseEl.classList.remove(\"show\");\n                }\n            });\n        },\n        patch_builder_options: [\n            {\n                target_name: 'ProductsRibbonOption',\n                target_element: 'selector',\n                method: 'add',\n                value: ProductPageOption.selector,\n            },\n        ],\n    };\n\n    setup() {\n        const mainEl = this.document.querySelector(ProductPageOption.selector);\n        if (mainEl) {\n            const productProduct = mainEl.querySelector('[data-oe-model=\"product.product\"]');\n            const productTemplate = mainEl.querySelector('[data-oe-model=\"product.template\"]');\n            this.productProductID = productProduct ? productProduct.dataset.oeId : null;\n            this.productTemplateID = productTemplate ? productTemplate.dataset.oeId : null;\n            this.model = \"product.template\";\n            if (this.productProductID) {\n                this.model = \"product.product\";\n            }\n            // Different targets\n            this.productDetailEl = mainEl.querySelector(\"#product_detail\");\n            this.productDetailMain = mainEl.querySelector(\"#product_detail_main\");\n            this.productPageCarousel = mainEl.querySelector(\"#o-carousel-product\");\n            this.productPageGrid = mainEl.querySelector(\"#o-grid-product\");\n        }\n    }\n\n    forceCarouselRedraw() {\n        if (!this.productPageCarousel) {\n            return;\n        }\n        const targetWindow = this.productPageCarousel.ownerDocument.defaultView || window;\n        const resizeEvent = new Event('resize');\n        targetWindow.dispatchEvent(resizeEvent);\n    }\n}\n\n// Base class for product page configuration actions\nexport class BasePreviewableProductPageAction extends PreviewableWebsiteConfigAction {\n    static dependencies = [...super.dependencies, \"productPageOption\"];\n    static rpcParameterName = null;\n    static shouldForceCarouselRedraw = true;\n\n    async apply({ editingElement, isPreviewing, params, value }) {\n        await super.apply({ editingElement, isPreviewing, params, value });\n\n        if (this.constructor.shouldForceCarouselRedraw) {\n            this.dependencies.productPageOption.forceCarouselRedraw();\n        }\n        if (!isPreviewing) {\n            await this.makeRpcCall(value);\n        }\n    }\n\n    async makeRpcCall(value) {\n        if (this.constructor.rpcParameterName) {\n            await rpc(\"/shop/config/website\", { [this.constructor.rpcParameterName]: value });\n        }\n    }\n}\nexport class ProductPageContainerWidthAction extends BasePreviewableProductPageAction {\n    static id = \"productPageContainerWidth\";\n    static rpcParameterName = \"product_page_container\";\n}\n\nexport class ProductPageContainerOrderAction extends BasePreviewableProductPageAction {\n    static id = \"productPageContainerOrder\";\n    static rpcParameterName = \"product_page_cols_order\";\n    static shouldForceCarouselRedraw = false;\n}\n\nexport class ProductPageImageRatioAction extends BasePreviewableProductPageAction {\n    static id = \"productPageImageRatio\";\n    static rpcParameterName = \"product_page_image_ratio\";\n}\n\nexport class ProductPageImageRatioMobileAction extends BasePreviewableProductPageAction {\n    static id = \"productPageImageRatioMobile\";\n    static rpcParameterName = \"product_page_image_ratio_mobile\";\n}\n\nexport class ProductPageImageWidthAction extends BasePreviewableProductPageAction {\n    static id = \"productPageImageWidth\";\n    static rpcParameterName = \"product_page_image_width\";\n}\n\nexport class ProductPageImageGridSpacingAction extends BasePreviewableProductPageAction {\n    static id = \"productPageImageGridSpacing\";\n    static rpcParameterName = \"product_page_image_spacing\";\n}\n\nexport class ProductPageImageRoundnessAction extends BasePreviewableProductPageAction {\n    static id = \"productPageRoundness\";\n    static rpcParameterName = \"product_page_image_roundness\";\n}\n\nexport class ProductPageImageLayoutAction extends WebsiteConfigAction {\n    static id = \"productPageImageLayout\";\n    static dependencies = [...super.dependencies, \"customizeWebsite\", \"productPageOption\"];\n    isApplied({ editingElement: productDetailMainEl, value }) {\n        return productDetailMainEl.dataset.image_layout === value;\n    }\n    getValue({ editingElement: productDetailMainEl }) {\n        return productDetailMainEl.dataset.image_layout;\n    }\n    async apply({ value }) {\n        return rpc(\"/shop/config/website\", { product_page_image_layout: value });\n    }\n}\n\nexport class BaseProductPageAction extends BuilderAction {\n    static id = \"baseProductPage\";\n    setup() {\n        this.reload = {};\n        const mainEl = this.document.querySelector(ProductPageOption.selector);\n        if (mainEl) {\n            const productProduct = mainEl.querySelector('[data-oe-model=\"product.product\"]');\n            const productTemplate = mainEl.querySelector('[data-oe-model=\"product.template\"]');\n            this.productProductID = productProduct ? productProduct.dataset.oeId : null;\n            this.productTemplateID = productTemplate ? productTemplate.dataset.oeId : null;\n            this.model = \"product.template\";\n            if (this.productProductID) {\n                this.model = \"product.product\";\n            }\n            // Different targets\n            this.productDetailMain = mainEl.querySelector(\"#product_detail_main\");\n            this.productPageCarousel = mainEl.querySelector(\"#o-carousel-product\");\n            this.productPageGrid = mainEl.querySelector(\"#o-grid-product\");\n        }\n    }\n    getSelectedVariantValues(el) {\n        const containerEl = el.querySelector(\".js_add_cart_variants\");\n        return wSaleUtils.getSelectedAttributeValues(containerEl);\n    }\n\n    async extraMediaSave(el, type, attachments, extraImageEls) {\n        if (type === \"image\") {\n            for (const index in attachments) {\n                const attachment = attachments[index];\n                if (attachment.mimetype.startsWith(\"image/\")) {\n                    if ([\"image/gif\", \"image/svg+xml\"].includes(attachment.mimetype)) {\n                        continue;\n                    }\n                    await this.convertAttachmentToWebp(attachment, extraImageEls[index]);\n                }\n            }\n        }\n        await rpc(\"/shop/product/extra-media\", {\n            media: attachments,\n            type: type,\n            product_product_id: this.productProductID,\n            product_template_id: this.productTemplateID,\n            combination_ids: this.getSelectedVariantValues(el),\n        });\n    }\n\n    async convertAttachmentToWebp(attachment, imageEl) {\n        // This method is widely adapted from onFileUploaded in ImageField.\n        // Upon change, make sure to verify whether the same change needs\n        // to be applied on both sides.\n        if (await isImageCorsProtected(imageEl)) {\n            // The image is CORS protected; do not transform it into webp\n            return;\n        }\n        // Generate alternate sizes and format for reports.\n        const imgEl = document.createElement(\"img\");\n        imgEl.src = imageEl.src;\n        await new Promise((resolve) => imgEl.addEventListener(\"load\", resolve));\n        const originalSize = Math.max(imgEl.width, imgEl.height);\n        const smallerSizes = [1920, 1024, 512, 256, 128].filter((size) => size < originalSize);\n        const extension = attachment.name.match(/\\.(jpe?|pn)g$/i)?.[0] ?? \".jpeg\";\n        const webpName = attachment.name.replace(extension, \".webp\");\n        const format = extension.substr(1).toLowerCase().replace(/^jpg$/, \"jpeg\");\n        const mimetype = `image/${format}`;\n        let referenceId = undefined;\n        for (const size of [originalSize, ...smallerSizes]) {\n            const ratio = size / originalSize;\n            const canvas = document.createElement(\"canvas\");\n            canvas.width = imgEl.width * ratio;\n            canvas.height = imgEl.height * ratio;\n            const ctx = canvas.getContext(\"2d\");\n            ctx.fillStyle = \"transparent\";\n            ctx.fillRect(0, 0, canvas.width, canvas.height);\n            ctx.drawImage(\n                imgEl,\n                0,\n                0,\n                imgEl.width,\n                imgEl.height,\n                0,\n                0,\n                canvas.width,\n                canvas.height\n            );\n            const [resizedId] = await this.services.orm.call(\"ir.attachment\", \"create_unique\", [\n                [\n                    {\n                        name: webpName,\n                        description: size === originalSize ? \"\" : `resize: ${size}`,\n                        datas: canvas.toDataURL(\"image/webp\").split(\",\")[1],\n                        res_id: referenceId,\n                        res_model: \"ir.attachment\",\n                        mimetype: \"image/webp\",\n                    },\n                ],\n            ]);\n            if (size === originalSize) {\n                attachment.original_id = attachment.id;\n                attachment.id = resizedId;\n                attachment.image_src = `/web/image/${resizedId}-autowebp/${attachment.name}`;\n                attachment.mimetype = \"image/webp\";\n            }\n            referenceId = referenceId || resizedId; // Keep track of original.\n            await this.services.orm.call(\"ir.attachment\", \"create_unique\", [\n                [\n                    {\n                        name: attachment.name,\n                        description: `format: ${format}`,\n                        datas: canvas.toDataURL(mimetype).split(\",\")[1],\n                        res_id: resizedId,\n                        res_model: \"ir.attachment\",\n                        mimetype: mimetype,\n                    },\n                ],\n            ]);\n        }\n    }\n}\n\nexport class ProductPageImageGridColumnsAction extends BaseProductPageAction {\n    static id = \"productPageImageGridColumns\";\n\n    isApplied({ value }) {\n        return (parseInt(this.productPageGrid?.dataset.grid_columns) || 1) === value;\n    }\n    getValue() {\n        parseInt(this.productPageGrid?.dataset.grid_columns) || 1;\n    }\n    async apply({ value }) {\n        this.productPageGrid.dataset.grid_columns = value;\n        await rpc(\"/shop/config/website\", {\n            product_page_grid_columns: value,\n        });\n    }\n}\nexport class ProductReplaceMainImageAction extends BaseProductPageAction {\n    static id = \"productReplaceMainImage\";\n    static dependencies = [...super.dependencies, \"media\", \"media_website\"];\n    setup() {\n        super.setup();\n        this.reload = false;\n    }\n    apply({ editingElement: productDetailMainEl }) {\n        // Emulate click on the main image of the carousel.\n        const image = productDetailMainEl.querySelector(\n            `[data-oe-model=\"${this.model}\"][data-oe-field=image_1920] img`\n        );\n        this.dependencies.media.openMediaDialog({\n            multiImages: false,\n            visibleTabs: [\"IMAGES\"],\n            node: productDetailMainEl,\n            save: (imgEl, selectedMedia) => {\n                const attachment = selectedMedia[0];\n                if ([\"image/gif\", \"image/svg+xml\"].includes(attachment.mimetype)) {\n                    image.src = attachment.image_src;\n                    return;\n                }\n                const originalSize = Math.max(imgEl.width, imgEl.height);\n                const ratio = Math.min(originalSize, 1920) / originalSize;\n                const canvas = document.createElement(\"canvas\");\n                canvas.width = parseInt(imgEl.width * ratio);\n                canvas.height = parseInt(imgEl.height * ratio);\n                const ctx = canvas.getContext(\"2d\")\n                ctx.fillStyle = \"transparent\";\n                ctx.fillRect(0, 0, canvas.width, canvas.height);\n                ctx.drawImage(imgEl, 0, 0);\n                image.src = canvas.toDataURL(\"image/webp\");\n                const { model, productProductID: productID, productTemplateID: templateID } = this;\n                const resID = parseInt(model === \"product.product\" ? productID : templateID);\n                this.services.orm.write(model, [resID], {\n                    image_1920: image.src.split(\",\")[1],\n                });\n            },\n        });\n    }\n}\n\nexport class ProductAddExtraImageAction extends BaseProductPageAction {\n    static id = \"productAddExtraImage\";\n    static dependencies = [...super.dependencies, \"media\"];\n    async apply({ editingElement: el }) {\n        // Prompts the user for images, then saves the new images.\n        if (this.model === \"product.template\") {\n            this.services.notification.add(\n                'Pictures will be added to the main image. Use \"Instant\" attributes to set pictures on each variants',\n                { type: \"info\" }\n            );\n        }\n        await new Promise((resolve) => {\n            const onClose = this.dependencies.media.openMediaDialog({\n                addFieldImage: true,\n                multiImages: true,\n                visibleTabs: [\"IMAGES\", \"VIDEOS\"],\n                node: el,\n                // Kinda hack-ish but the regular save does not get the information we need\n                save: async (imgEls, selectedMedia, activeTab) => {\n                    if (selectedMedia.length) {\n                        const type =\n                            activeTab === TABS[\"IMAGES\"].id ? \"image\" : \"video\";\n                        await this.extraMediaSave(el, type, selectedMedia, imgEls);\n                    }\n                },\n            });\n            onClose.then(resolve);\n        });\n    }\n}\nexport class ProductRemoveAllExtraImagesAction extends BaseProductPageAction {\n    static id = \"productRemoveAllExtraImages\";\n    async apply({ editingElement: el }) {\n        // Removes all extra-images from the product.\n        await rpc(`/shop/product/clear-images`, {\n            model: this.model,\n            product_product_id: this.productProductID,\n            product_template_id: this.productTemplateID,\n            combination_ids: this.getSelectedVariantValues(el),\n        })\n    }\n}\n\nregistry.category(\"website-plugins\").add(ProductPageOptionPlugin.id, ProductPageOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { onWillStart, useState } from \"@odoo/owl\";\n\nexport class ProductsRibbonOption extends BaseOptionComponent {\n    static template = 'website_sale.ProductsRibbonOptionPlugin';\n    static dependencies = ['productsRibbonOptionPlugin'];\n\n    setup() {\n        super.setup();\n\n        const {loadInfo, getCount} = this.dependencies.productsRibbonOptionPlugin;\n        this.count = useState(getCount());\n\n        this.state = useState({\n            ribbons: [],\n            ribbonEditMode: false,\n        });\n\n        onWillStart(async () => {\n            this.state.ribbons = await loadInfo();\n        });\n    }\n}\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { SNIPPET_SPECIFIC_NEXT } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { reactive } from \"@odoo/owl\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { ProductsRibbonOption } from \"./product_ribbon_options\";\n\nexport class ProductHeaderShopOption2 extends ProductsRibbonOption {\n    static name = 'ProductsRibbonOption';\n    static selector = \"#products_grid .oe_product\";\n    static editableOnly = false;\n    static groups = ['website.group_website_designer'];\n}\n\nclass ProductsRibbonOptionPlugin extends Plugin {\n    static id = 'productsRibbonOptionPlugin';\n    static dependencies = ['history'];\n    static shared = [\n        'getRibbonsObject',\n        'setRibbonObject',\n        'addRibbon',\n        'getRibbons',\n        'setRibbon',\n        'deleteRibbon',\n        '_setRibbon',\n        'setProductTemplateID',\n        'getProductTemplateID',\n        'addProductTemplatesRibbons',\n        'loadInfo',\n        'getCount',\n    ];\n    count = reactive({ value: 0 });\n\n    resources = {\n        builder_options: [\n            withSequence(SNIPPET_SPECIFIC_NEXT, ProductHeaderShopOption2),\n        ],\n        builder_actions: {\n            SetRibbonAction,\n            CreateRibbonAction,\n            ModifyRibbonAction,\n            DeleteRibbonAction,\n        },\n    };\n\n    setup() {\n        this.positionClasses = { left: \"o_left\", right: \"o_right\" };\n        this.styleClasses = { ribbon: \"o_wsale_ribbon\", tag: \"o_wsale_badge\" };\n        this.productTemplatesRibbons = [];\n        this.editMode = false;\n    }\n    getCount() {\n        return this.count;\n    }\n\n    async loadInfo() {\n        if (!this.ribbons) {\n            const result = await this.services.orm.searchRead(\n                'product.ribbon',\n                [['assign', '=', 'manual']],\n                ['id', 'name', 'bg_color', 'text_color', 'position', 'style']\n            );\n            this.ribbons = reactive(result);\n        }\n\n        this.ribbonsObject = this.ribbons.reduce((acc, ribbon) => {\n            acc[ribbon.id] = ribbon;\n            return acc;\n        }, {});\n\n        this.originalRibbons = JSON.parse(JSON.stringify(this.ribbonsObject));\n\n        return this.ribbons;\n    }\n\n    async _setRibbon(editingElement, ribbon, save = true) {\n        const ribbonId = ribbon.id;\n        const editableBody = editingElement.ownerDocument.body;\n        editingElement.dataset.ribbonId = ribbonId;\n\n        // Update all ribbons with this ID\n        const ribbons = editableBody.ownerDocument.querySelectorAll(\n            `[data-ribbon-id=\"${ribbonId}\"]`,\n        );\n\n        for (const ribbonElement of ribbons) {\n            ribbonElement.textContent = ribbon.name;\n            ribbonElement.classList.remove('o_wsale_ribbon', 'o_wsale_badge', 'o_right', 'o_left');\n            if (ribbonElement.classList.contains('d-none')) {\n                ribbonElement.classList.remove('d-none');\n            }\n\n            ribbonElement.classList.add(\n                this.positionClasses[ribbon.position],\n                this.styleClasses[ribbon.style],\n            );\n            ribbonElement.style.backgroundColor = ribbon.bg_color || \"\";\n            ribbonElement.style.color = ribbon.text_color || \"\";\n        }\n\n        return save ? await this._saveRibbons() : \"\";\n    }\n\n    async _saveRibbons() {\n        const originalIds = Object.keys(this.originalRibbons).map((id) => parseInt(id));\n        const currentIds = this.ribbons.map((ribbon) => parseInt(ribbon.id));\n        const created = this.ribbons.filter((ribbon) => !originalIds.includes(ribbon.id));\n        const deletedIds = originalIds.filter((id) => !currentIds.includes(id));\n        const modified = this.ribbons.filter((ribbon) => {\n            if (created.includes(ribbon)) {\n                return false;\n            }\n            const original = this.originalRibbons[ribbon.id];\n            return Object.entries(ribbon).some(([key, value]) => value !== original[key]);\n        });\n\n        const createdRibbonProms = [];\n        let createdRibbonIds;\n        if (created.length > 0) {\n            createdRibbonProms.push(\n                this.services.orm.create(\n                    'product.ribbon',\n                    created.map((ribbon) => {\n                        ribbon = Object.assign({}, ribbon);\n                        this.originalRibbons[ribbon.id] = ribbon;\n                        delete ribbon.id;\n                        return ribbon;\n                    })\n                ).then((ids) => (createdRibbonIds = ids))\n            );\n        }\n        await Promise.all(createdRibbonProms);\n\n        const localToServer = Object.assign(\n            this.ribbonsObject,\n            Object.fromEntries(\n                created.map((ribbon, index) => [\n                    ribbon.id,\n                    { ...this.ribbonsObject[ribbon.id], id: createdRibbonIds[index] },\n                ])\n            ),\n            {\n                false: {\n                    id: \"\",\n                },\n            }\n        );\n\n        const proms = [];\n        for (const ribbon of modified) {\n            const ribbonData = {\n                name: ribbon.name,\n                bg_color: ribbon.bg_color,\n                text_color: ribbon.text_color,\n                position: ribbon.position,\n                style: ribbon.style,\n            };\n            const serverId = localToServer[ribbon.id]?.id || ribbon.id;\n            proms.push(this.services.orm.write('product.ribbon', [serverId], ribbonData));\n            this.originalRibbons[ribbon.id] = Object.assign({}, ribbon);\n        }\n\n        if (deletedIds.length > 0) {\n            proms.push(this.services.orm.unlink('product.ribbon', deletedIds));\n        }\n\n        await Promise.all(proms);\n\n        // Building the final template to ribbon-id map so that we can remove duplicate entries\n        const finalTemplateRibbons = this.productTemplatesRibbons.reduce(\n            (acc, { templateId, ribbonId }) => {\n                acc[templateId] = ribbonId;\n                return acc;\n            }, {},\n        );\n        // Inverting the relationship so that we have all templates that have the same ribbon to\n        // reduce RPCs\n        const ribbonTemplates = {};\n        for (const [templateId, ribbonId] of Object.entries(finalTemplateRibbons)) {\n            const rid = ribbonTemplates[ribbonId] ||= [];\n            rid.push(parseInt(templateId));\n        }\n\n        const promises = [];\n        for (const ribbonId in ribbonTemplates) {\n            const templateIds = ribbonTemplates[ribbonId];\n            const parsedId = parseInt(ribbonId);\n            const validRibbonId = currentIds.includes(parsedId) ? ribbonId : false;\n            promises.push(\n                this.services.orm.write('product.template', templateIds, {\n                    website_ribbon_id: localToServer[validRibbonId]?.id || false,\n                })\n            );\n        }\n\n        return Promise.all(promises);\n    }\n\n    /**\n     * Deletes a ribbon.\n     *\n     */\n    deleteRibbon(editingElement) {\n        const ribbonId = parseInt(editingElement.querySelector('.o_ribbons')?.dataset.ribbonId);\n        if (this.ribbonsObject[ribbonId]) {\n            const ribbonIndex = this.ribbons.findIndex(ribbon => ribbon.id === ribbonId);\n            if (ribbonIndex !== -1 ) {\n                this.ribbons.splice(ribbonIndex, 1);\n            }\n            delete this.ribbonsObject[ribbonId];\n\n            // update \"reactive\" count to trigger rerendering the BuilderSelect component (which\n            // has the value as a t-key)\n            this.count.value++;\n        }\n        const isProductPage = editingElement.ownerDocument.querySelector('#product_detail');\n        this.productTemplateID = parseInt(\n            editingElement\n                .querySelector('[data-oe-model=\"product.template\"]')\n                .getAttribute(\"data-oe-id\")\n        );\n        const ribbons = editingElement.ownerDocument.querySelectorAll(\n            `[data-ribbon-id=\"${ribbonId}\"]`\n        );\n        ribbons.forEach((ribbonElement) => {\n            ribbonElement.classList.add(\"d-none\");\n            ribbonElement.dataset.ribbonId = \"\";\n            let templateId;\n            if (isProductPage) {\n                templateId = this.productTemplateID;\n            } else {\n                // Find the product template ID from the ribbon element's parent form\n                const productForm = ribbonElement.closest('form.oe_product_cart');\n                const templateElement = productForm?.querySelector('[data-oe-model=\"product.template\"]');\n                templateId = templateElement ? parseInt(templateElement.getAttribute('data-oe-id')) : null;\n            }\n            if (templateId && !isNaN(templateId)) {\n                this.productTemplatesRibbons.push({\n                    templateId: templateId,\n                    ribbonId: false,\n                });\n            }\n        });\n        this._saveRibbons();\n    }\n    getProductTemplateID() {\n        return this.productTemplateID;\n    }\n    setProductTemplateID(id) {\n        this.productTemplateID = id\n    }\n    addProductTemplatesRibbons(value) {\n        this.productTemplatesRibbons.push(value);\n    }\n    getRibbonsObject() {\n        return this.ribbonsObject;\n    }\n    setRibbonObject(key, value) {\n        this.ribbonsObject[key] = value;\n    }\n    addRibbon(value) {\n        this.ribbons.push(value);\n    }\n    getRibbons() {\n        return this.ribbons;\n    }\n    setRibbon(key, value){\n        const index = this.ribbons.findIndex((ribbon) => ribbon.id == key);\n        if (index !== -1) {\n            this.ribbons[index] = value;\n        }\n    }\n}\n\nclass SetRibbonAction extends BuilderAction {\n    static id = 'setRibbon';\n    static dependencies = ['productsRibbonOptionPlugin'];\n    setup(){\n        this.ribbonOptions = this.dependencies.productsRibbonOptionPlugin\n    }\n    isApplied({ editingElement, value }) {\n        const ribbonId = parseInt(\n            editingElement.querySelector('.o_ribbons')?.dataset.ribbonId,\n        );\n        const match = !ribbonId || !this.ribbonOptions.getRibbonsObject().hasOwnProperty(ribbonId)\n            ? ''\n            : ribbonId;\n        return match === value;\n    }\n    apply({ isPreviewing, editingElement, value }) {\n        const productTemplateID = parseInt(\n            editingElement\n                .querySelector('[data-oe-model=\"product.template\"]')\n                .getAttribute('data-oe-id')\n        );\n        this.ribbonOptions.setProductTemplateID(productTemplateID)\n        this.ribbonOptions.addProductTemplatesRibbons({\n            templateId: productTemplateID,\n            ribbonId: value,\n        });\n\n        const ribbon = this.ribbonOptions.getRibbonsObject()[value] || {\n            id: '',\n            name: '',\n            bg_color: '',\n            text_color: '',\n            position: 'left',\n            style: 'ribbon',\n        };\n\n        return this.ribbonOptions._setRibbon(\n            editingElement.querySelector('.o_ribbons'),\n            ribbon,\n            !isPreviewing,\n        );\n    }\n}\nclass CreateRibbonAction extends BuilderAction {\n    static id = 'createRibbon';\n    static dependencies = ['productsRibbonOptionPlugin']\n    setup(){\n        this.ribbonOptions = this.dependencies.productsRibbonOptionPlugin\n    }\n    apply({ editingElement }) {\n        const productTemplateId = editingElement\n            .querySelector('[data-oe-model=\"product.template\"]')\n            .getAttribute('data-oe-id')\n        this.ribbonOptions.setProductTemplateID(parseInt(\n            productTemplateId\n        ));\n        const ribbonId = Date.now();\n        this.ribbonOptions.addProductTemplatesRibbons({\n            templateId: productTemplateId,\n            ribbonId: ribbonId,\n        });\n        const ribbon = reactive({\n            id: ribbonId,\n            name: 'Ribbon Name',\n            bg_color: '',\n            text_color: 'purple',\n            position: 'left',\n            style: 'ribbon',\n        });\n        this.ribbonOptions.addRibbon(ribbon);\n        this.ribbonOptions.setRibbonObject(ribbonId, ribbon);\n        return this.ribbonOptions._setRibbon(editingElement.querySelector('.o_ribbons'), ribbon);\n    }\n}\nclass ModifyRibbonAction extends BuilderAction {\n    static id = 'modifyRibbon';\n    static dependencies = ['productsRibbonOptionPlugin', 'history'];\n    setup() {\n        this.ribbonOptions = this.dependencies.productsRibbonOptionPlugin\n    }\n    getValue({ editingElement, params }) {\n        const ribbonId = parseInt(\n            editingElement.querySelector('.o_ribbons')?.dataset.ribbonId\n        );\n        if (!ribbonId || !this.ribbonOptions.getRibbonsObject().hasOwnProperty(ribbonId)) {\n            return;\n        }\n\n        return this.ribbonOptions.getRibbonsObject()[ribbonId][params.mainParam];\n    }\n    isApplied({ editingElement, params, value }) {\n        let ribbonId = parseInt(\n            editingElement.querySelector('.o_ribbons')?.dataset.ribbonId\n        );\n        if (!ribbonId || !this.ribbonOptions.getRibbonsObject().hasOwnProperty(ribbonId)) {\n            return;\n        }\n        return this.ribbonOptions.getRibbonsObject()[ribbonId][params.mainParam] === value;\n    }\n    apply({ editingElement, params, value }) {\n        const isPreviewMode = this.dependencies.history.getIsPreviewing();\n        const ribbonEl = editingElement.querySelector('.o_ribbons')\n        const setting = params.mainParam;\n        const ribbonId = parseInt(ribbonEl.dataset.ribbonId);\n        const previousRibbon = this.ribbonOptions.getRibbonsObject()[ribbonId];\n        this.ribbonOptions.setRibbonObject(ribbonId, {...previousRibbon, [setting]: value});\n        this.ribbonOptions.setRibbon(ribbonId, {...previousRibbon, [setting]: value});\n        const res = this.ribbonOptions._setRibbon(ribbonEl, {...previousRibbon, [setting]: value}, !isPreviewMode);\n        if(isPreviewMode){\n            this.ribbonOptions.setRibbonObject(ribbonId, previousRibbon)\n            this.ribbonOptions.setRibbon(ribbonId, previousRibbon)\n        }\n        return res\n    }\n}\nclass DeleteRibbonAction extends BuilderAction {\n    static id = 'deleteRibbon';\n    static dependencies = ['productsRibbonOptionPlugin'];\n    async apply({ editingElement }) {\n        const save = await new Promise((resolve) => {\n            this.services.dialog.add(ConfirmationDialog, {\n                body: _t(\"Are you sure you want to delete this ribbon?\"),\n                confirm: () => resolve(true),\n                cancel: () => resolve(false),\n            });\n        });\n        if (!save) {\n            return;\n        }\n        return this.dependencies.productsRibbonOptionPlugin.deleteRibbon(editingElement);\n    }\n}\n\nregistry.category('website-plugins').add(\n    ProductsRibbonOptionPlugin.id, ProductsRibbonOptionPlugin,\n);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { onMounted, onWillDestroy, useState } from \"@odoo/owl\";\n\nexport class ProductsDesignPanel extends BaseOptionComponent {\n    static template = \"website_sale.ProductsDesignPanel\";\n    static components = {\n        ...BaseOptionComponent.components,\n    };\n    static props = {\n        label: { type: String, optional: true },\n        recordName: { type: String, optional: true },\n        showLists: { type: Boolean, optional: true },\n        showSecondaryImage: { type: Boolean, optional: true },\n        openByDefault: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        label: \"Design\",\n        showLists: true,\n        showSecondaryImage: false,\n        openByDefault: false,\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({ overlayVisible: false });\n        this.needsDbPersistence = this.props.recordName?.length > 0;\n\n        onMounted(() => {\n            this.setupActionConnections();\n            this.registerWithPlugin();\n\n            if (this.props.openByDefault) {\n                this.openDesignOverlay();\n            }\n        });\n\n        onWillDestroy(() => {\n            this.unregisterFromPlugin();\n        });\n    }\n\n    registerWithPlugin() {\n        const plugin = this.env.editor.shared.productsDesignPanel;\n        if (plugin) {\n            plugin.registerPanel(this);\n        }\n    }\n\n    unregisterFromPlugin() {\n        const plugin = this.env.editor.shared.productsDesignPanel;\n        if (plugin) {\n            plugin.unregisterPanel(this);\n        }\n    }\n\n    setupActionConnections() {\n        // Set panel reference for setGap action\n        const builderActions = this.env.editor.shared.builderActions;\n        const action = builderActions.getAction('setGap');\n\n        if (action && action.setPanel) {\n            action.setPanel(this);\n        }\n    }\n\n    openDesignOverlay() {\n        this.state.overlayVisible = true;\n    }\n\n    closeDesignOverlay() {\n        this.state.overlayVisible = false;\n    }\n\n    onBackdropClick(ev) {\n        if (ev.target === ev.currentTarget) {\n            this.closeDesignOverlay();\n        }\n    }\n}\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { ProductsDesignPanel } from \"./products_design_panel\";\n\nexport class ProductsDesignPanelPlugin extends Plugin {\n    static id = \"productsDesignPanel\";\n    static dependencies = [\"builderActions\", \"builderComponents\"];\n    static shared = [\"registerPanel\", \"unregisterPanel\"];\n\n    resources = {\n        builder_actions: {\n            classActionWithSave: ClassActionWithSuggestedAction,\n            setGap: SetGapAction,\n        },\n        builder_components: {\n            ProductsDesignPanel,\n        },\n        save_handlers: this.onSave.bind(this),\n        change_current_options_containers_listeners: () => {\n            this.panels.forEach(panel => {\n                if (panel.state.overlayVisible) {\n                    panel.closeDesignOverlay();\n                }\n            });\n        },\n    };\n\n    setup() {\n        this.panels = new Set();\n    }\n\n    registerPanel(panel) {\n        this.panels.add(panel);\n    }\n\n    unregisterPanel(panel) {\n        this.panels.delete(panel);\n    }\n\n    async onSave() {\n        const persistentPanels = Array.from(this.panels).filter(panel => panel.needsDbPersistence);\n\n        for (const panel of persistentPanels) {\n            const pageEl = panel.env.getEditingElement();\n            const updateData = {};\n\n            // Save gap\n            if (pageEl.dataset.gapToSave !== undefined) {\n                updateData.shop_gap = pageEl.dataset.gapToSave;\n            }\n\n            // Scan DOM for all classes with o_wsale_products_opt_ prefix\n            const currentClasses = Array.from(pageEl.classList);\n            const productOptClasses = currentClasses.filter(className =>\n                className.startsWith('o_wsale_products_opt_')\n            );\n\n            // Always save the classes field (empty string if no classes found)\n            updateData[panel.props.recordName] = productOptClasses.join(' ');\n\n            // Early return only if no classes and no gap to save\n            if (productOptClasses.length === 0 && pageEl.dataset.gapToSave === undefined) {\n                continue;\n            }\n\n            // Save data\n            if (Object.keys(updateData).length > 0) {\n                await rpc(\"/shop/config/website\", updateData);\n            }\n        }\n    }\n}\n\n/**\n * Handles suggestedClasses with clean slate approach and delegates to classAction/setClassRange\n */\nclass ClassActionWithSuggestedAction extends BuilderAction {\n    static id = \"classActionWithSave\";\n    static dependencies = [\"builderActions\"];\n\n    setup() {\n        this.classAction = this.dependencies.builderActions.getAction('classAction');\n        this.setClassRangeAction = this.dependencies.builderActions.getAction('setClassRange');\n    }\n\n    getPriority(context) {\n        const targetAction = Array.isArray(context.params.className) ?\n            this.setClassRangeAction : this.classAction;\n        return targetAction.getPriority?.(context) || 0;\n    }\n\n    isApplied(context) {\n        // Transform parameters to match expected format\n        const { className } = context.params;\n\n        const targetAction = Array.isArray(className) ?\n            this.setClassRangeAction : this.classAction;\n\n        // Transform context to match what the target action expects\n        const delegatedContext = {\n            ...context,\n            params: { mainParam: className }\n        };\n\n        return targetAction.isApplied(delegatedContext);\n    }\n\n    getValue(context) {\n        const { className } = context.params;\n        const targetAction = Array.isArray(className) ?\n            this.setClassRangeAction : this.classAction;\n\n        const delegatedContext = {\n            ...context,\n            params: { mainParam: className }\n        };\n\n        return targetAction.getValue?.(delegatedContext);\n    }\n\n    apply(context) {\n        const { editingElement, params: { className, suggestedClasses } } = context;\n\n        if (suggestedClasses) {\n            this.applySuggestedClasses(editingElement, suggestedClasses);\n        }\n\n        // Delegate DOM manipulation to appropriate action\n        const targetAction = Array.isArray(className) ? this.setClassRangeAction : this.classAction;\n\n        const delegatedContext = {\n            ...context,\n            params: { mainParam: className }\n        };\n\n        return targetAction.apply(delegatedContext);\n    }\n\n    clean(context) {\n        const { editingElement, params: { className, suggestedClasses } } = context;\n\n        if (suggestedClasses) {\n            this.cleanSuggestedClasses(editingElement, suggestedClasses);\n        }\n\n        // Delegate DOM manipulation to appropriate action\n        const targetAction = Array.isArray(className) ? this.setClassRangeAction : this.classAction;\n\n        return targetAction.clean({\n            ...context,\n            params: { mainParam: className }\n        });\n    }\n\n    /**\n     * Clean slate approach: Remove ALL o_wsale_products_opt_* classes, then add positive ones from suggestedClasses\n     */\n    applySuggestedClasses(editingElement, suggestedClasses) {\n        if (!suggestedClasses || typeof suggestedClasses !== 'string') {\n            return;\n        }\n\n        // 1. Clean slate: Remove ALL existing design classes\n        const currentClasses = Array.from(editingElement.classList);\n        const designClasses = currentClasses.filter(cls => cls.startsWith('o_wsale_products_opt_'));\n        designClasses.forEach(cls => editingElement.classList.remove(cls));\n\n        // 2. Apply new classes\n        const newClasses = suggestedClasses.trim().split(/\\s+/).filter(cls => cls && !cls.startsWith('!'));\n        newClasses.forEach(cls => editingElement.classList.add(cls));\n    }\n\n    /**\n     * Reverse of applySuggestedClasses for clean operations\n     */\n    cleanSuggestedClasses(editingElement, suggestedClasses) {\n        if (!suggestedClasses || typeof suggestedClasses !== 'string') {\n            return;\n        }\n\n        // Remove the positive classes that were added\n        const classesToRemove = suggestedClasses.trim().split(/\\s+/).filter(cls => cls && !cls.startsWith('!'));\n        classesToRemove.forEach(cls => editingElement.classList.remove(cls));\n    }\n}\n\nclass SetGapAction extends BuilderAction {\n    static id = \"setGap\";\n\n    isApplied() {\n        return true;\n    }\n\n    getValue({ editingElement }) {\n        return editingElement.style.getPropertyValue(\"--o-wsale-products-grid-gap\");\n    }\n\n    apply({ editingElement, value }) {\n        editingElement.style.setProperty(\"--o-wsale-products-grid-gap\", value);\n        if (this.panel?.needsDbPersistence) {\n            editingElement.dataset.gapToSave = value;\n        }\n    }\n\n    setPanel(panel) {\n        this.panel = panel;\n    }\n}\n\nregistry.category(\"website-plugins\").add(ProductsDesignPanelPlugin.id, ProductsDesignPanelPlugin);\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { onWillStart, onMounted, useState, useRef } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class ProductsItemOption extends BaseOptionComponent {\n    static template = \"website_sale.ProductsItemOptionPlugin\";\n    static dependencies = [\"productsItemOptionPlugin\"];\n    static selector = \"#products_grid .oe_product\";\n    static title = _t(\"Product\");\n    static groups = [\"website.group_website_designer\"];\n    static editableOnly = false;\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.tableRef = useRef(\"table\");\n\n        const { loadInfo, getItemSize, getCount } = this.dependencies.productsItemOptionPlugin;\n\n        this.state = useState({\n            itemSize: getItemSize(),\n            count: getCount(),\n        });\n\n        this.productsGridTableEl = this.env.getEditingElement().closest(\".o_wsale_products_grid_table\");\n\n        this.domState = useDomState(() => {\n\n            // If /shop page layout is list, do not display Size option\n            const displaySizeOption = !this.productsGridTableEl?.classList.contains(\"o_wsale_products_opt_layout_list\");\n\n            if(displaySizeOption && this.state.itemSize) {\n                this.addClassToTableCells(this.state.itemSize.x, this.state.itemSize.y, \"selected\");\n            }\n\n            return {\n                displaySizeOption,\n            }\n        });\n\n        onWillStart(async () => {\n            this.defaultSort = await loadInfo();\n\n            // need to display \"re-order\" option only if shop_default_sort is 'website_sequence asc'\n            this.displayReOrder = this.defaultSort[0].shop_default_sort === \"website_sequence asc\";\n            const pprValue = this.productsGridTableEl.style.getPropertyValue('--o-wsale-ppr').trim();\n            this.maxWidth = parseInt(pprValue) || 5;\n        });\n\n        onMounted(() => {\n            if (this.domState.displaySizeOption) {\n                this.addClassToTableCells(this.state.itemSize.x, this.state.itemSize.y, \"selected\");\n            }\n        });\n    }\n\n    addClassToTableCells(x, y, className) {\n        const table = this.tableRef.el;\n\n        // By default, this.domState.displaySizeOption is undefined, so the table is not displayed\n        // We need to check if the table is visible before adding classes to the cells\n        if(!table) { return; }\n\n        const rows = table.rows;\n\n        for (let row = 0; row < y; row++) {\n            const cells = rows[row].cells;\n            for (let col = 0; col < x; col++) {\n                cells[col].classList.add(className);\n            }\n        }\n    }\n\n    _onTableMouseEnter(ev) {\n        ev.currentTarget.classList.add(\"oe_hover\");\n    }\n\n    _onTableMouseLeave(ev) {\n        ev.currentTarget.classList.remove(\"oe_hover\");\n    }\n\n    _onTableCellMouseOver(i, j) {\n        const allCells = this.tableRef.el.querySelectorAll(\"td.select\");\n\n        for (const cell of allCells) {\n            cell.classList.remove(\"select\");\n        }\n\n        this.addClassToTableCells(j + 1, i + 1, \"select\");\n    }\n\n    _onTableCellMouseClick(i, j) {\n        const allCells = this.tableRef.el.querySelectorAll(\"td.selected\");\n\n        for (const cell of allCells) {\n            cell.classList.remove(\"selected\");\n        }\n\n        this.addClassToTableCells(j + 1, i + 1, \"selected\");\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { ProductsItemOption } from \"./products_item_option\";\nimport { reactive } from \"@odoo/owl\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\n\nclass ProductsItemOptionPlugin extends Plugin {\n    static id = \"productsItemOptionPlugin\";\n    static shared = [\n        \"setItemSize\",\n        \"setProductTemplateID\",\n        \"getProductTemplateID\",\n        \"loadInfo\",\n        \"getItemSize\",\n        \"getCount\",\n    ];\n    itemSize = reactive({ x: 1, y: 1 });\n\n    resources = {\n        builder_options: [ProductsItemOption],\n        builder_actions: {\n            SetItemSizeAction,\n            ChangeSequenceAction,\n        },\n    };\n\n    setup() {\n        this.currentWebsiteId = this.services.website.currentWebsiteId;\n        this.editMode = false;\n    }\n\n    async loadInfo() {\n        this.defaultSort = await this.getDefaultSort();\n        return this.defaultSort;\n    }\n\n    getItemSize() {\n        return this.itemSize;\n    }\n    getCount() {\n        return this.count;\n    }\n\n    async getDefaultSort() {\n        return (\n            this.defaultSort ||\n            (await this.services.orm.searchRead(\n                \"website\",\n                [[\"id\", \"=\", this.currentWebsiteId]],\n                [\"shop_default_sort\"]\n            ))\n        );\n    }\n    setItemSize(x, y) {\n        this.itemSize.x = x;\n        this.itemSize.y = y;\n    }\n    setProductTemplateID(value) {\n        this.productTemplateID = value;\n    }\n    getProductTemplateID() {\n        return this.productTemplateID;\n    }\n}\n\nexport class SetItemSizeAction extends BuilderAction {\n    static id = \"setItemSize\";\n    static dependencies = [\"productsItemOptionPlugin\"];\n    setup() {\n        this.productItemPlugin = this.dependencies.productsItemOptionPlugin;\n        this.reload = {};\n    }\n    isApplied({ editingElement, value: [i, j] }) {\n        if (\n            parseInt(editingElement.dataset.rowspan || 1) - 1 === i &&\n            parseInt(editingElement.dataset.colspan || 1) - 1 === j\n        ) {\n            this.productItemPlugin.setItemSize(j + 1, i + 1);\n            return true;\n        }\n        return false;\n    }\n    apply({ editingElement, value: [i, j] }) {\n        const x = j + 1;\n        const y = i + 1;\n\n        this.productItemPlugin.setProductTemplateID(\n            parseInt(\n                editingElement\n                    .querySelector('[data-oe-model=\"product.template\"]')\n                    .getAttribute(\"data-oe-id\")\n            )\n        );\n        return rpc(\"/shop/config/product\", {\n            product_id: this.productItemPlugin.getProductTemplateID(),\n            x: x,\n            y: y,\n        });\n    }\n}\nexport class ChangeSequenceAction extends BuilderAction {\n    static id = \"changeSequence\";\n    static dependencies = [\"productsItemOptionPlugin\"];\n    setup() {\n        this.productItemPlugin = this.dependencies.productsItemOptionPlugin;\n        this.reload = {};\n    }\n    apply({ editingElement, value }) {\n        this.productItemPlugin.setProductTemplateID(\n            parseInt(\n                editingElement\n                    .querySelector('[data-oe-model=\"product.template\"]')\n                    .getAttribute(\"data-oe-id\")\n            )\n        );\n        return rpc(\"/shop/config/product\", {\n            product_id: this.productItemPlugin.getProductTemplateID(),\n            sequence: value,\n        });\n    }\n}\n\nregistry.category(\"website-plugins\").add(ProductsItemOptionPlugin.id, ProductsItemOptionPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { products_sort_mapping } from \"@website_sale/website_builder/shared\";\n\nexport class ProductsListPageOption extends BaseOptionComponent {\n    static template = \"website_sale.ProductsListPageOption\";\n    static selector = \"#o_wsale_container\";\n    static title = _t(\"Products Page\");\n    static groups = [\"website.group_website_designer\"];\n    static editableOnly = false;\n\n    setup() {\n        super.setup();\n        this.products_sort_mapping = products_sort_mapping;\n    }\n}\n", "import { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { PreviewableWebsiteConfigAction } from \"@website/builder/plugins/customize_website_plugin\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { ProductsListPageOption } from \"@website_sale/website_builder/products_list_page_option\";\n\nclass ProductsListPageOptionPlugin extends Plugin {\n    static id = \"productsListPageOptionPlugin\";\n\n    resources = {\n        builder_options: [ProductsListPageOption],\n        builder_actions: {\n            SetShopContainerAction,\n            SetPpgAction,\n            SetPprAction,\n            SetDefaultSortAction,\n        },\n    };\n}\nexport class SetShopContainerAction extends PreviewableWebsiteConfigAction {\n    static id = \"setShopContainer\";\n\n    async apply({ editingElement: productDetailMainEl, isPreviewing, params, value }) {\n        await super.apply({ editingElement: productDetailMainEl, isPreviewing, params, value });\n\n        if (!isPreviewing) {\n            await rpc(\"/shop/config/website\", { 'shop_page_container': value });\n        }\n    }\n}\nexport class SetPpgAction extends BuilderAction {\n    static id = \"setPpg\";\n    setup() {\n        this.reload = {};\n    }\n    getValue({ editingElement }) {\n        return parseInt(editingElement.dataset.ppg);\n    }\n    apply({ value }) {\n        const PPG_LIMIT = 10000;\n        let ppg = parseInt(value);\n        if (!ppg || ppg < 1) {\n            return false;\n        }\n        ppg = Math.min(ppg, PPG_LIMIT);\n        return rpc(\"/shop/config/website\", { shop_ppg: ppg });\n    }\n}\nexport class SetPprAction extends BuilderAction {\n    static id = \"setPpr\";\n    setup() {\n        this.reload = {};\n    }\n    isApplied({ editingElement, value }) {\n        return parseInt(editingElement.dataset.ppr) === value;\n    }\n    apply({ value }) {\n        const ppr = parseInt(value);\n        return rpc(\"/shop/config/website\", { shop_ppr: ppr });\n    }\n}\nexport class SetDefaultSortAction extends BuilderAction {\n    static id = \"setDefaultSort\";\n    setup() {\n        this.reload = {};\n    }\n    isApplied({ editingElement, value }) {\n        return editingElement.dataset.defaultSort === value;\n    }\n    apply({ value }) {\n        return rpc(\"/shop/config/website\", { shop_default_sort: value });\n    }\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(ProductsListPageOptionPlugin.id, ProductsListPageOptionPlugin);\n", "import { products_sort_mapping } from \"@website_sale/website_builder/shared\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nclass ProductsSearchbarOptionPlugin extends Plugin {\n    static id = \"productsSearchbarOption\";\n\n    resources = {\n        // 'name asc' is already part of the general sorting methods of this\n        // snippet.\n        searchbar_option_order_by_items: products_sort_mapping\n            .filter((sort) => sort.query !== \"name asc\")\n            .map((query_and_label) => ({\n                label: query_and_label.label,\n                orderBy: query_and_label.query,\n                dependency: \"search_products_opt\",\n            })),\n        searchbar_option_display_items: [\n            {\n                label: _t(\"Description\"),\n                dataAttribute: \"displayDescription\",\n                dependency: \"search_products_opt\",\n            },\n            {\n                label: _t(\"Category\"),\n                dataAttribute: \"displayExtraLink\",\n                dependency: \"search_products_opt\",\n            },\n            {\n                label: _t(\"Price\"),\n                dataAttribute: \"displayDetail\",\n                dependency: \"search_products_opt\",\n            },\n            {\n                label: _t(\"Image\"),\n                dataAttribute: \"displayImage\",\n                dependency: \"search_products_opt\",\n            },\n        ],\n    };\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(ProductsSearchbarOptionPlugin.id, ProductsSearchbarOptionPlugin);\n", "import { _t } from \"@web/core/l10n/translation\";\n\n// TODO: need to fetch _get_product_sort_mapping to remove duplicate data\nexport const products_sort_mapping = [\n    {\n        query: \"website_sequence asc\",\n        label: _t(\"Featured\"),\n    },\n    {\n        query: \"publish_date desc\",\n        label: _t(\"Newest Arrivals\"),\n    },\n    {\n        query: \"name asc\",\n        label: _t(\"Name (A-Z)\"),\n    },\n    {\n        query: \"list_price asc\",\n        label: _t(\"Price - Low to High\"),\n    },\n    {\n        query: \"list_price desc\",\n        label: _t(\"Price - High to Low\"),\n    },\n];\n", "import { HEADER_ELEMENTS } from \"@website/builder/plugins/options/header/header_option_plugin\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\nimport { after } from \"@html_builder/utils/option_sequence\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class ShowEmptyOption extends BaseOptionComponent {\n    static template = \"website_sale.ShowEmptyOption\";\n    static selector = \"#wrapwrap > header\";\n    static groups = [\"website.group_website_designer\"];\n    static editableOnly = false;\n}\n\nclass WebsiteSaleShowEmptyOptionPlugin extends Plugin {\n    static id = \"showEmptyOption\";\n    resources = {\n        builder_options: [\n            withSequence(after(HEADER_ELEMENTS), ShowEmptyOption),\n        ],\n    };\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(WebsiteSaleShowEmptyOptionPlugin.id, WebsiteSaleShowEmptyOptionPlugin);\n", "import { markup } from '@odoo/owl';\nimport { browser } from '@web/core/browser/browser';\nimport { _t } from '@web/core/l10n/translation';\nimport { setElementContent } from '@web/core/utils/html';\n\nfunction animateClone($cart, $elem, offsetTop, offsetLeft) {\n    if (!$cart.length) {\n        return Promise.resolve();\n    }\n    $cart.removeClass('d-none').find('.o_animate_blink').addClass('o_red_highlight o_shadow_animation').delay(500).queue(function () {\n        $(this).removeClass(\"o_shadow_animation\").dequeue();\n    }).delay(2000).queue(function () {\n        $(this).removeClass(\"o_red_highlight\").dequeue();\n    });\n    return new Promise(function (resolve, reject) {\n        if(!$elem) resolve();\n        var $imgtodrag = $elem.find('img').eq(0);\n        if ($imgtodrag.length) {\n            var $imgclone = $imgtodrag.clone()\n                .offset({\n                    top: $imgtodrag.offset().top,\n                    left: $imgtodrag.offset().left\n                })\n                .removeClass()\n                .addClass('o_website_sale_animate')\n                .appendTo(document.body)\n                .css({\n                    // Keep the same size on cloned img.\n                    width: $imgtodrag.width(),\n                    height: $imgtodrag.height(),\n                })\n                .animate({\n                    top: $cart.offset().top + offsetTop,\n                    left: $cart.offset().left + offsetLeft,\n                    width: 75,\n                    height: 75,\n                }, 500);\n\n            $imgclone.animate({\n                width: 0,\n                height: 0,\n            }, function () {\n                resolve();\n                $(this).detach();\n            });\n        } else {\n            resolve();\n        }\n    });\n}\n\n/**\n * Returns the closest product form to a given element if exists.\n * Required for product pages with full-width or no images where the \"Add to cart\" button can be \n * outside of the form.\n *\n * @param { HTMLElement } element - Reference to an HTML element in the DOM.\n * @returns { HTMLFormElement|undefined }\n */\nfunction getClosestProductForm(element){\n    return element.closest('form') ?? element.closest('.js_product')?.querySelector('form');\n}\n\n/**\n * Updates both navbar cart\n * @param {Object} data\n * @return {void}\n */\nfunction updateCartNavBar(data) {\n    browser.sessionStorage.setItem('website_sale_cart_quantity', data.cart_quantity);\n    // Mobile and Desktop elements have to be updated.\n    const cartQuantityElements = document.querySelectorAll('.my_cart_quantity');\n    for(const cartQuantityElement of cartQuantityElements) {\n        if (data.cart_quantity === 0) {\n            cartQuantityElement.classList.add('d-none');\n        } else {\n            const cartIconElement = document.querySelector('li.o_wsale_my_cart');\n            cartIconElement.classList.remove('d-none');\n            cartQuantityElement.classList.remove('d-none');\n            cartQuantityElement.classList.add('o_mycart_zoom_animation');\n            setTimeout(() => {\n                cartQuantityElement.textContent = data.cart_quantity;\n                cartQuantityElement.classList.remove('o_mycart_zoom_animation');\n            }, 300);\n        }\n    }\n\n    $(\".js_cart_lines\").first().before(data['website_sale.cart_lines']).end().remove();\n\n    updateCartSummary(data);\n\n    // Adjust the cart's left column width to accommodate the cart summary (right column). The left\n    // column of an empty cart initially takes the full width, but adding products (e.g. via quick \n    // reorder) enables the cart summary on the right.\n    document.querySelector('.oe_cart').classList.toggle('col-lg-7', !!data.cart_quantity);\n\n    if (data.cart_ready) {\n        document.querySelector(\"a[name='website_sale_main_button']\")?.classList.remove('disabled');\n    } else {\n        document.querySelector(\"a[name='website_sale_main_button']\")?.classList.add('disabled');\n    }\n}\n\n/**\n * Update the cart summary.\n *\n * @param {Object} data\n * @return {void}\n */\nfunction updateCartSummary(data) {\n    if (data['website_sale.shorter_cart_summary']) {\n        const shorterCartSummaryEl = document.querySelector('.o_wsale_shorter_cart_summary');\n        setElementContent(shorterCartSummaryEl, markup(data['website_sale.shorter_cart_summary']));\n    }\n    if (data['website_sale.total']) {\n        document.querySelectorAll('div.o_cart_total').forEach(\n            div => div.innerHTML = data['website_sale.total']\n        );\n    }\n}\n\n/**\n * Update the quick reorder side panel.\n *\n * @param {Object} data\n * @return {void}\n */\nfunction updateQuickReorderSidebar(data) {\n    const quickReorderButton  = document.getElementById('quick_reorder_button');\n    document.querySelectorAll('.o_wsale_quick_reorder_line_group').forEach(el => el.remove());\n    if (data['website_sale.quick_reorder_history'].trim()) {\n        document.querySelector('#quick_reorder_sidebar .offcanvas-body').insertAdjacentHTML(\n            'afterbegin', data['website_sale.quick_reorder_history']\n        );\n        quickReorderButton.removeAttribute('disabled');\n        quickReorderButton.parentElement.title = \"\";\n    } else {\n        quickReorderButton.click();\n        quickReorderButton.setAttribute('disabled', 'true');\n        quickReorderButton.parentElement.title = _t(\"No previous products available for reorder.\");\n    }\n}\n\n/**\n * Displays `message` in an alert box at the top of the page if it's a\n * non-empty string.\n *\n * @param {string | null} message\n */\nfunction showWarning(message) {\n    if (!message) {\n        return;\n    }\n    var $page = $('.oe_website_sale');\n    var cart_alert = $page.children('#data_warning');\n    if (!cart_alert.length) {\n        cart_alert = $(\n            '<div class=\"alert alert-danger alert-dismissible\" role=\"alert\" id=\"data_warning\">' +\n                '<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\"></button> ' +\n                '<span></span>' +\n            '</div>').prependTo($page);\n    }\n    cart_alert.children('span:last-child').text(message);\n}\n\n/**\n * Return the selected attribute values from the given container.\n *\n * @param {Element} container the container to look into\n */\nfunction getSelectedAttributeValues(container) {\n    return Array.from(container.querySelectorAll(\n        'input.js_variant_change:checked, select.js_variant_change'\n    )).map(el => parseInt(el.value));\n}\n\nexport default {\n    animateClone: animateClone,\n    getClosestProductForm: getClosestProductForm,\n    updateCartNavBar: updateCartNavBar,\n    showWarning: showWarning,\n    getSelectedAttributeValues: getSelectedAttributeValues,\n    updateQuickReorderSidebar: updateQuickReorderSidebar,\n};\n", "import { registry } from \"@web/core/registry\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ImportedFooterTemplateChoice } from \"./imported_footer_template_choice\";\n\nclass ImportedFooterPlugin extends Plugin {\n    static id = \"importedFooter\";\n    resources = {\n        footer_templates_providers: async () => {\n            const website_id = this.services.website.currentWebsite.id;\n            const view = `website_generator.template_ws_custom_footer_${website_id}`;\n            if (\n                (await this.services.orm.searchCount(\"ir.ui.view\", [[\"key\", \"=\", view]], {\n                    context: { active_test: false },\n                })) === 0\n            ) {\n                return [];\n            }\n\n            return [\n                {\n                    key: view,\n                    Component: ImportedFooterTemplateChoice,\n                    props: {\n                        title: _t(\"Imported Footer\"),\n                        view,\n                        varName: `imported-footer-${website_id}`,\n                        imgSrc: \"/website_generator/static/src/img/footer_template_imported.svg\",\n                    },\n                },\n            ];\n        },\n    };\n}\n\nregistry.category(\"website-plugins\").add(ImportedFooterPlugin.id, ImportedFooterPlugin);\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { markup } from \"@odoo/owl\";\n\nexport class ImportedFooterTemplateChoice extends BaseOptionComponent {\n    static template = \"website_generator.ImportedFooterTemplateChoice\";\n    static props = { title: String, view: String, varName: String, imgSrc: String };\n    setup() {\n        this.label = markup`<Img attrs=\"{ style: 'width: 100%;' }\" src=\"${this.props.imgSrc}\"/>`;\n    }\n}\n", "\nimport { Plugin } from \"@html_editor/plugin\";\nimport { BuilderAction } from \"@html_builder/core/builder_action\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class WishlistPageOption extends BaseOptionComponent {\n    static template = \"website_sale_wishlist.WishlistPageOption\";\n    static selector = \".o_wishlist_table\"\n    static editableOnly = false;\n    static title = _t(\"Wishlist Page\");\n    static groups = [\"website.group_website_designer\"];\n}\n\nclass WishlistPageOptionPlugin extends Plugin {\n    static id = \"wishlistPageOption\";\n    resources = {\n        builder_options: WishlistPageOption,\n        builder_actions: {\n            WishlistGridColumnsAction,\n            WishlistMobileColumnsAction,\n            WishlistSetGapAction\n        },\n        save_handlers: this.onSave.bind(this),\n    };\n\n    async onSave() {\n        const wishlistEl = this.editable.querySelector(\".o_wishlist_table\");\n        if (!wishlistEl) return;\n\n        const gridColumns = parseInt(wishlistEl.dataset.wishlistGridColumns) || 5;\n        const mobileColumns = parseInt(wishlistEl.dataset.wishlistMobileColumns) || 2;\n        const gap = wishlistEl.style.getPropertyValue(\"--o-wsale-wishlist-grid-gap\") || \"16px\";\n\n        return rpc(\"/shop/config/website\", {\n            wishlist_grid_columns: gridColumns,\n            wishlist_mobile_columns: mobileColumns,\n            wishlist_gap: gap,\n        });\n    }\n}\n\nexport class WishlistGridColumnsAction extends BuilderAction {\n    static id = \"wishlistGridColumns\";\n\n    isApplied({ editingElement, value }) {\n        return parseInt(editingElement.dataset.wishlistGridColumns) === value;\n    }\n    getValue({ editingElement }) {\n        return parseInt(editingElement.dataset.wishlistGridColumns);\n    }\n    apply({ editingElement, value }) {\n        editingElement.dataset.wishlistGridColumns = value;\n    }\n}\n\nexport class WishlistMobileColumnsAction extends BuilderAction {\n    static id = \"wishlistMobileColumns\";\n\n    isApplied({ editingElement, value }) {\n        return parseInt(editingElement.dataset.wishlistMobileColumns) === value;\n    }\n    getValue({ editingElement }) {\n        return parseInt(editingElement.dataset.wishlistMobileColumns);\n    }\n    apply({ editingElement, value }) {\n        editingElement.dataset.wishlistMobileColumns = value;\n    }\n}\n\nexport class WishlistSetGapAction extends BuilderAction {\n    static id = \"wishlistSetGap\";\n\n    isApplied() {\n        return true;\n    }\n\n    getValue({ editingElement }) {\n        return editingElement.style.getPropertyValue(\"--o-wsale-wishlist-grid-gap\");\n    }\n\n    apply({ editingElement, value }) {\n        editingElement.style.setProperty(\"--o-wsale-wishlist-grid-gap\", value);\n    }\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(WishlistPageOptionPlugin.id, WishlistPageOptionPlugin);\n", "import { patch } from \"@web/core/utils/patch\";\nimport { Many2OneAction } from \"@html_builder/plugins/many2one_option_plugin\";\n\npatch(Many2OneAction, {\n    dependencies: [...Many2OneAction.dependencies, \"history\"],\n});\n\npatch(Many2OneAction.prototype, {\n    apply(args) {\n        super.apply(args);\n        const { editingElement, value } = args;\n        const { id } = JSON.parse(value);\n        const { oeId, oeField } = editingElement.dataset;\n\n        if (oeField === \"author_id\") {\n            for (const node of this.editable.querySelectorAll(\n                `[data-oe-model=\"blog.post\"][data-oe-id=\"${oeId}\"][data-oe-field=\"author_avatar\"]`\n            )) {\n                node.querySelector(\"img\").src = `/web/image/res.partner/${id}/avatar_1024`;\n                // We do not want to save it to the server\n                // TODO: a more general approach for editing records that are used at different parts of the page\n                this.dependencies.history.ignoreDOMMutations(() => {\n                    node.classList.remove(\"o_dirty\");\n                });\n            }\n        }\n    },\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { useDomState } from \"@html_builder/core/utils\";\nimport { CoverPropertiesOption } from \"@website/builder/plugins/options/cover_properties_option\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npatch(CoverPropertiesOption.prototype, {\n    setup() {\n        super.setup();\n        this.blogState = useDomState((editingElement) => ({\n            isRegularCover: editingElement.classList.contains(\"o_wblog_post_page_cover_regular\"),\n        }));\n    },\n\n    coverSizeLabel(className) {\n        return this.blogState.isRegularCover\n            ? blogCoverSizeClassLabels[className]\n            : super.coverSizeLabel(className);\n    },\n});\n\nconst blogCoverSizeClassLabels = {\n    o_full_screen_height: _t(\"Large\"),\n    o_half_screen_height: _t(\"Medium\"),\n    cover_auto: _t(\"Tiny\"),\n};\n", "import { BaseOptionComponent } from \"@html_builder/core/utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nexport class BlogPageOption extends BaseOptionComponent {\n    static template = \"website_blog.BlogPageOption\";\n    static selector = \"main:has(#o_wblog_post_main)\";\n    static title = _t(\"Blog Page\");\n    static groups = [\"website.group_website_designer\"];\n    static editableOnly = false;\n}\n\nexport class BlogPageOptionPlugin extends Plugin {\n    static id = \"blogPageOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [BlogPageOption],\n    };\n}\n\nregistry.category(\"website-plugins\").add(BlogPageOptionPlugin.id, BlogPageOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport class BlogPostPageOption extends BaseOptionComponent {\n    static template = \"website_blog.blogPostPageOption\";\n    static selector = \"main:has(#o_wblog_index_content)\";\n    static title = _t(\"Blogs Page\");\n    static groups = [\"website.group_website_designer\"];\n    static editableOnly = false;\n}\n\nexport class BlogPostPageOptionPlugin extends Plugin {\n    static id = \"blogPostPageOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [BlogPostPageOption],\n    };\n}\n\nregistry.category(\"website-plugins\").add(BlogPostPageOptionPlugin.id, BlogPostPageOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\n\nexport class BlogPostTitlePlugin extends Plugin {\n    static id = \"blogPostTitle\";\n    resources = {\n        before_setup_editor_handlers: () => {\n            // TODO: Remove in master and modify the XML\n            // view addons/website_blog/views/website_blog_templates.xml for the\n            // two templates (opt_blog_cover_post_fullwidth_design,\n            // opt_blog_cover_post)\n            const latestPostsEl = this.editable.querySelector(\n                \"#o_wblog_blog_top .h1.o_not_editable\"\n            );\n            latestPostsEl?.classList.remove(\"o_not_editable\");\n        },\n    };\n}\n\nregistry.category(\"website-plugins\").add(BlogPostTitlePlugin.id, BlogPostTitlePlugin);\nregistry.category(\"website-translation-plugins\").add(BlogPostTitlePlugin.id, BlogPostTitlePlugin);\n", "import { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\n\nexport class BlogPostTagsOption extends BaseOptionComponent {\n    static template = \"website_blog.BlogPostTagsOption\";\n    static selector = \".o_wblog_post_page_cover[data-res-model='blog.post']\";\n    static editableOnly = false;\n\n    setup() {\n        super.setup();\n        this.domState = useDomState((el) => ({\n            blogId: parseInt(el.dataset.resId),\n        }));\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { registry } from \"@web/core/registry\";\nimport { BlogPostTagsOption } from \"./blog_post_tags_option\";\n\nclass BlogPostTagsOptionPlugin extends Plugin {\n    static id = \"blogPostTagsOption\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: [BlogPostTagsOption],\n    };\n}\n\nregistry.category(\"website-plugins\").add(BlogPostTagsOptionPlugin.id, BlogPostTagsOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nclass BlogSearchbarOptionPlugin extends Plugin {\n    static id = \"blogSearchbarOption\";\n\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        searchbar_option_order_by_items: [\n            {\n                label: _t(\"Date (old to new)\"),\n                orderBy: \"published_date asc\",\n                dependency: \"search_blogs_opt\",\n            },\n            {\n                label: _t(\"Date (new to old)\"),\n                orderBy: \"published_date desc\",\n                dependency: \"search_blogs_opt\",\n            },\n        ],\n        searchbar_option_display_items: [\n            {\n                label: _t(\"Description\"),\n                dataAttribute: \"displayDescription\",\n                dependency: \"search_blogs_opt\",\n            },\n            {\n                label: _t(\"Publication Date\"),\n                dataAttribute: \"displayDetail\",\n                dependency: \"search_blogs_opt\",\n            },\n        ],\n    };\n}\n\nregistry.category(\"website-plugins\").add(BlogSearchbarOptionPlugin.id, BlogSearchbarOptionPlugin);\n", "import { onWillStart, useState } from \"@odoo/owl\";\nimport { BaseOptionComponent, useDomState } from \"@html_builder/core/utils\";\nimport { useDynamicSnippetOption } from \"@website/builder/plugins/options/dynamic_snippet_hook\";\n\nexport class DynamicSnippetBlogPostsOption extends BaseOptionComponent {\n    static template = \"website_blog.DynamicSnippetBlogPostsOption\";\n    static dependencies = [\"dynamicSnippetBlogPostsOption\"];\n    static selector = \".s_dynamic_snippet_blog_posts\";\n    setup() {\n        super.setup();\n        const { fetchBlogs, getModelNameFilter } = this.dependencies.dynamicSnippetBlogPostsOption;\n        this.dynamicOptionParams = useDynamicSnippetOption(getModelNameFilter());\n        this.blogState = useState({\n            blogs: [],\n        });\n        onWillStart(async () => {\n            this.blogState.blogs.push(...(await fetchBlogs()));\n        });\n        this.templateKeyState = useDomState((el) => ({\n            templateKey: el.dataset.templateKey,\n        }));\n    }\n    showPictureSizeOption() {\n        return [\n            \"website_blog.dynamic_filter_template_blog_post_big_picture\",\n            \"website_blog.dynamic_filter_template_blog_post_horizontal\",\n            \"website_blog.dynamic_filter_template_blog_post_card\",\n        ].includes(this.templateKeyState.templateKey);\n    }\n    showTeaserOption() {\n        return [\n            \"website_blog.dynamic_filter_template_blog_post_list\",\n            \"website_blog.dynamic_filter_template_blog_post_card\",\n        ].includes(this.templateKeyState.templateKey);\n    }\n    showDateOption() {\n        return [\n            \"website_blog.dynamic_filter_template_blog_post_list\",\n            \"website_blog.dynamic_filter_template_blog_post_horizontal\",\n            \"website_blog.dynamic_filter_template_blog_post_card\",\n            \"website_blog.dynamic_filter_template_blog_post_single_full\",\n            \"website_blog.dynamic_filter_template_blog_post_single_aside\",\n            \"website_blog.dynamic_filter_template_blog_post_single_circle\",\n        ].includes(this.templateKeyState.templateKey);\n    }\n    showCategoryOption() {\n        return [\n            \"website_blog.dynamic_filter_template_blog_post_list\",\n            \"website_blog.dynamic_filter_template_blog_post_horizontal\",\n            \"website_blog.dynamic_filter_template_blog_post_card\",\n            \"website_blog.dynamic_filter_template_blog_post_single_full\",\n            \"website_blog.dynamic_filter_template_blog_post_single_aside\",\n            \"website_blog.dynamic_filter_template_blog_post_single_circle\",\n            \"website_blog.dynamic_filter_template_blog_post_single_badge\",\n        ].includes(this.templateKeyState.templateKey);\n    }\n    showNewTagOption() {\n        return (\n            this.templateKeyState.templateKey ===\n            \"website_blog.dynamic_filter_template_blog_post_single_badge\"\n        );\n    }\n    showHoverEffectOption() {\n        return (\n            this.templateKeyState.templateKey ===\n            \"website_blog.dynamic_filter_template_blog_post_big_picture\"\n        );\n    }\n}\n", "import {\n    DYNAMIC_SNIPPET,\n    setDatasetIfUndefined,\n} from \"@website/builder/plugins/options/dynamic_snippet_option_plugin\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\nimport { DynamicSnippetBlogPostsOption } from \"./dynamic_snippet_blog_posts_option\";\n\n/**\n * @typedef { Object } DynamicSnippetBlogPostsOptionShared\n * @property { DynamicSnippetBlogPostsOptionPlugin['fetchBlogs'] } fetchBlogs\n * @property { DynamicSnippetBlogPostsOptionPlugin['getModelNameFilter'] } getModelNameFilter\n */\n\nclass DynamicSnippetBlogPostsOptionPlugin extends Plugin {\n    static id = \"dynamicSnippetBlogPostsOption\";\n    static dependencies = [\"dynamicSnippetOption\"];\n    static shared = [\"fetchBlogs\",\"getModelNameFilter\"];\n    modelNameFilter = \"blog.post\";\n    /** @type {import(\"plugins\").WebsiteResources} */\n    resources = {\n        builder_options: withSequence(DYNAMIC_SNIPPET, DynamicSnippetBlogPostsOption),\n        on_snippet_dropped_handlers: this.onSnippetDropped.bind(this),\n    };\n    setup() {\n        this.blogs = undefined;\n    }\n    getModelNameFilter() {\n        return this.modelNameFilter;\n    }\n    async onSnippetDropped({ snippetEl }) {\n        if (snippetEl.matches(DynamicSnippetBlogPostsOption.selector)) {\n            setDatasetIfUndefined(snippetEl, \"filterByBlogId\", -1);\n            await this.dependencies.dynamicSnippetOption.setOptionsDefaultValues(\n                snippetEl,\n                this.modelNameFilter\n            );\n        }\n    }\n    async fetchBlogs() {\n        if (!this.blogs) {\n            this.blogs = this._fetchBlogs();\n        }\n        return this.blogs;\n    }\n    async _fetchBlogs() {\n        // TODO put in an utility function\n        const websiteDomain = [\n            \"|\",\n            [\"website_id\", \"=\", false],\n            [\"website_id\", \"=\", this.services.website.currentWebsite.id],\n        ];\n        return this.services.orm.searchRead(\"blog.blog\", websiteDomain, [\"id\", \"name\"]);\n    }\n}\n\nregistry\n    .category(\"website-plugins\")\n    .add(DynamicSnippetBlogPostsOptionPlugin.id, DynamicSnippetBlogPostsOptionPlugin);\n", "import { DEFAULT } from \"@html_builder/utils/option_sequence\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { BaseOptionComponent } from \"@html_builder/core/utils\";\n\nexport const FORUMS_INDEX = DEFAULT;\n\nexport class ForumPageOption extends BaseOptionComponent {\n    static template = \"website_forum.forumPageOption\";\n    static selector = \"main:has(#o_wforum_forums_index_list)\";\n    static title = _t(\"Forum Page\");\n    static groups = [\"website.group_website_designer\"];\n    static editableOnly = false;\n}\n\nclass ForumPageOptionPlugin extends Plugin {\n    static id = \"forumPageOption\";\n    resources = {\n        builder_options: [\n            withSequence(FORUMS_INDEX, ForumPageOption),\n        ],\n    };\n}\n\nregistry.category(\"website-plugins\").add(ForumPageOptionPlugin.id, ForumPageOptionPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nclass ForumSearchbarOptionPlugin extends Plugin {\n    static id = \"forumSearchbarOption\";\n\n    resources = {\n        searchbar_option_order_by_items: [\n            {\n                label: _t(\"Date (old to new)\"),\n                orderBy: \"write_date asc\",\n                dependency: \"search_forums_opt\",\n            },\n            {\n                label: _t(\"Date (new to old)\"),\n                orderBy: \"write_date desc\",\n                dependency: \"search_forums_opt\",\n            },\n        ],\n        searchbar_option_display_items: [\n            {\n                label: _t(\"Description\"),\n                dataAttribute: \"displayDescription\",\n                dependency: \"search_forums_opt\",\n            },\n            {\n                label: _t(\"Date\"),\n                dataAttribute: \"displayDetail\",\n                dependency: \"search_forums_opt\",\n            },\n        ],\n    };\n}\n\nregistry.category(\"website-plugins\").add(ForumSearchbarOptionPlugin.id, ForumSearchbarOptionPlugin);\n"], "file": "/web/assets/1/8a8763b/website.website_builder_assets.js", "sourceRoot": "../../../../"}